/*
 * Copyright (C) 2018 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.spi;

import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import dagger.internal.codegen.ComponentProcessor;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class SpiPluginTest {
  @Test
  public void moduleBinding() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "",
            "@Module",
            "interface TestModule {",
            "  @Provides",
            "  static int provideInt() {",
            "    return 0;",
            "  }",
            "}");

    Compilation compilation =
        javac()
            .withProcessors(new ComponentProcessor())
            .withOptions(
                "-Aerror_on_binding=java.lang.Integer",
                "-Adagger.fullBindingGraphValidation=ERROR",
                "-Adagger.pluginsVisitFullBindingGraphs=ENABLED")
            .compile(module);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message("[FailingPlugin] Bad Binding: @Provides int test.TestModule.provideInt()"))
        .inFile(module)
        .onLineContaining("interface TestModule");
  }

  @Test
  public void dependencyTraceAtBinding() {
    JavaFileObject foo =
        JavaFileObjects.forSourceLines(
            "test.Foo",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Foo {",
            "  @Inject Foo() {}",
            "}");
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface TestComponent {",
            "  Foo foo();",
            "}");

    Compilation compilation =
        javac()
            .withProcessors(new ComponentProcessor())
            .withOptions("-Aerror_on_binding=test.Foo")
            .compile(component, foo);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Binding: @Inject test.Foo()",
                "    test.Foo is requested at",
                "        test.TestComponent.foo()"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
  }

  @Test
  public void dependencyTraceAtDependencyRequest() {
    JavaFileObject foo =
        JavaFileObjects.forSourceLines(
            "test.Foo",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Foo {",
            "  @Inject Foo(Duplicated inFooDep) {}",
            "}");
    JavaFileObject duplicated =
        JavaFileObjects.forSourceLines(
            "test.Duplicated",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Duplicated {",
            "  @Inject Duplicated() {}",
            "}");
    JavaFileObject entryPoint =
        JavaFileObjects.forSourceLines(
            "test.EntryPoint",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class EntryPoint {",
            "  @Inject EntryPoint(Foo foo, Duplicated dup1, Duplicated dup2) {}",
            "}");
    JavaFileObject chain1 =
        JavaFileObjects.forSourceLines(
            "test.Chain1",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Chain1 {",
            "  @Inject Chain1(Chain2 chain) {}",
            "}");
    JavaFileObject chain2 =
        JavaFileObjects.forSourceLines(
            "test.Chain2",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Chain2 {",
            "  @Inject Chain2(Chain3 chain) {}",
            "}");
    JavaFileObject chain3 =
        JavaFileObjects.forSourceLines(
            "test.Chain3",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Chain3 {",
            "  @Inject Chain3(Foo foo) {}",
            "}");
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface TestComponent {",
            "  EntryPoint entryPoint();",
            "  Chain1 chain();",
            "}");

    CompilationFactory compilationFactory =
        new CompilationFactory(component, foo, duplicated, entryPoint, chain1, chain2, chain3);

    assertThat(compilationFactory.compilationWithErrorOnDependency("entryPoint"))
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Dependency: test.TestComponent.entryPoint() (entry point)",
                "    test.EntryPoint is requested at",
                "        test.TestComponent.entryPoint()"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
    assertThat(compilationFactory.compilationWithErrorOnDependency("dup1"))
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup1, …)",
                "    test.Duplicated is injected at",
                "        test.EntryPoint(…, dup1, …)",
                "    test.EntryPoint is requested at",
                "        test.TestComponent.entryPoint()"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
    assertThat(compilationFactory.compilationWithErrorOnDependency("dup2"))
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup2)",
                "    test.Duplicated is injected at",
                "        test.EntryPoint(…, dup2)",
                "    test.EntryPoint is requested at",
                "        test.TestComponent.entryPoint()"))
        .inFile(component)
        .onLineContaining("interface TestComponent");

    Compilation inFooDepCompilation =
        compilationFactory.compilationWithErrorOnDependency("inFooDep");
    assertThat(inFooDepCompilation)
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Dependency: test.Foo(inFooDep)",
                "    test.Duplicated is injected at",
                "        test.Foo(inFooDep)",
                "    test.Foo is injected at",
                "        test.EntryPoint(foo, …)",
                "    test.EntryPoint is requested at",
                "        test.TestComponent.entryPoint()",
                "The following other entry points also depend on it:",
                "    test.TestComponent.chain()"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
  }

  @Test
  public void dependencyTraceAtDependencyRequest_subcomponents() {
    JavaFileObject foo =
        JavaFileObjects.forSourceLines(
            "test.Foo",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Foo {",
            "  @Inject Foo() {}",
            "}");
    JavaFileObject entryPoint =
        JavaFileObjects.forSourceLines(
            "test.EntryPoint",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class EntryPoint {",
            "  @Inject EntryPoint(Foo foo) {}",
            "}");
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface TestComponent {",
            "  TestSubcomponent sub();",
            "}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.TestSubcomponent",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface TestSubcomponent {",
            "  EntryPoint childEntryPoint();",
            "}");

    CompilationFactory compilationFactory =
        new CompilationFactory(component, subcomponent, foo, entryPoint);
    assertThat(compilationFactory.compilationWithErrorOnDependency("childEntryPoint"))
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Dependency: "
                    + "test.TestSubcomponent.childEntryPoint() (entry point)",
                "    test.EntryPoint is requested at",
                "        test.TestSubcomponent.childEntryPoint()"
                    + " [test.TestComponent → test.TestSubcomponent]"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
    assertThat(compilationFactory.compilationWithErrorOnDependency("foo"))
        .hadErrorContaining(
            // TODO(ronshapiro): Maybe make the component path resemble a stack trace:
            //     test.TestSubcomponent is a child of
            //         test.TestComponent
            // TODO(dpb): Or invert the order: Child → Parent
            message(
                "[FailingPlugin] Bad Dependency: test.EntryPoint(foo)",
                "    test.Foo is injected at",
                "        test.EntryPoint(foo)",
                "    test.EntryPoint is requested at",
                "        test.TestSubcomponent.childEntryPoint() "
                    + "[test.TestComponent → test.TestSubcomponent]"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
  }

  @Test
  public void errorOnComponent() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface TestComponent {}");

    Compilation compilation =
        javac()
            .withProcessors(new ComponentProcessor())
            .withOptions("-Aerror_on_component")
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining("[FailingPlugin] Bad Component: test.TestComponent")
        .inFile(component)
        .onLineContaining("interface TestComponent");
  }

  @Test
  public void errorOnSubcomponent() {
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.TestSubcomponent",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface TestSubcomponent {}");
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface TestComponent {",
            "  TestSubcomponent subcomponent();",
            "}");

    Compilation compilation =
        javac()
            .withProcessors(new ComponentProcessor())
            .withOptions("-Aerror_on_subcomponents")
            .compile(component, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "[FailingPlugin] Bad Subcomponent: test.TestComponent → test.TestSubcomponent "
                + "[test.TestComponent → test.TestSubcomponent]")
        .inFile(component)
        .onLineContaining("interface TestComponent");
  }

  // SpiDiagnosticReporter uses a shortest path algorithm to determine a dependency trace to a
  // binding. Without modifications, this would produce a strange error if a shorter path exists
  // from one entrypoint, through a @Module.subcomponents builder binding edge, and to the binding
  // usage within the subcomponent. Therefore, when scanning for the shortest path, we only consider
  // BindingNodes so we don't cross component boundaries. This test exhibits this case.
  @Test
  public void shortestPathToBindingExistsThroughSubcomponentBuilder() {
    JavaFileObject chain1 =
        JavaFileObjects.forSourceLines(
            "test.Chain1",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Chain1 {",
            "  @Inject Chain1(Chain2 chain) {}",
            "}");
    JavaFileObject chain2 =
        JavaFileObjects.forSourceLines(
            "test.Chain2",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Chain2 {",
            "  @Inject Chain2(Chain3 chain) {}",
            "}");
    JavaFileObject chain3 =
        JavaFileObjects.forSourceLines(
            "test.Chain3",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class Chain3 {",
            "  @Inject Chain3(ExposedOnSubcomponent exposedOnSubcomponent) {}",
            "}");
    JavaFileObject exposedOnSubcomponent =
        JavaFileObjects.forSourceLines(
            "test.ExposedOnSubcomponent",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "class ExposedOnSubcomponent {",
            "  @Inject ExposedOnSubcomponent() {}",
            "}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.TestSubcomponent",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface TestSubcomponent {",
            "  ExposedOnSubcomponent exposedOnSubcomponent();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    TestSubcomponent build();",
            "  }",
            "}");
    JavaFileObject subcomponentModule =
        JavaFileObjects.forSourceLines(
            "test.SubcomponentModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module(subcomponents = TestSubcomponent.class)",
            "interface SubcomponentModule {}");
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "@Component(modules = SubcomponentModule.class)",
            "interface TestComponent {",
            "  Chain1 chain();",
            "  TestSubcomponent.Builder subcomponent();",
            "}");

    Compilation compilation =
        javac()
            .withProcessors(new ComponentProcessor())
            .withOptions("-Aerror_on_binding=test.ExposedOnSubcomponent")
            .compile(
                component,
                subcomponent,
                chain1,
                chain2,
                chain3,
                exposedOnSubcomponent,
                subcomponentModule);
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "[FailingPlugin] Bad Binding: @Inject test.ExposedOnSubcomponent()",
                "    test.ExposedOnSubcomponent is injected at",
                "        test.Chain3(exposedOnSubcomponent)",
                "    test.Chain3 is injected at",
                "        test.Chain2(chain)",
                "    test.Chain2 is injected at",
                "        test.Chain1(chain)",
                "    test.Chain1 is requested at",
                "        test.TestComponent.chain()",
                "The following other entry points also depend on it:",
                "    test.TestSubcomponent.exposedOnSubcomponent() "
                    + "[test.TestComponent → test.TestSubcomponent]"))
        .inFile(component)
        .onLineContaining("interface TestComponent");
  }

  @Test
  public void onPluginEnd() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.TestComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface TestComponent {}");
    Compilation compilation = javac().withProcessors(new ComponentProcessor()).compile(component);
    assertThat(compilation)
        .generatedFile(StandardLocation.SOURCE_OUTPUT, "", "onPluginEndTest.txt");
  }

  // This works around an issue in the opensource compile testing where only one diagnostic is
  // recorded per line. When multiple validation items resolve to the same entry point, we can
  // only see the first. This helper class makes it easier to compile all of the files in the test
  // multiple times with different options to single out each error
  private static class CompilationFactory {
    private final ImmutableList<JavaFileObject> javaFileObjects;

    CompilationFactory(JavaFileObject... javaFileObjects) {
      this.javaFileObjects = ImmutableList.copyOf(javaFileObjects);
    }

    private Compilation compilationWithErrorOnDependency(String dependencySimpleName) {
      return javac()
          .withProcessors(new ComponentProcessor())
          .withOptions("-Aerror_on_dependency=" + dependencySimpleName)
          .compile(javaFileObjects);
    }
  }

  private static String message(String... lines) {
    return Joiner.on("\n  ").join(lines);
  }
}
