/*
 * Copyright (C) 2015 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.TestUtils.message;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import javax.tools.JavaFileObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class MissingBindingSuggestionsTest {
  private static JavaFileObject injectable(String className, String constructorParams) {
    return JavaFileObjects.forSourceLines("test." + className,
        "package test;",
        "",
        "import javax.inject.Inject;",
        "",
        "class " + className +" {",
        "  @Inject " + className + "(" + constructorParams + ") {}",
        "}");
  }

  private static JavaFileObject emptyInterface(String interfaceName) {
    return JavaFileObjects.forSourceLines("test." + interfaceName,
        "package test;",
        "",
        "import javax.inject.Inject;",
        "",
        "interface " + interfaceName +" {}");
  }

  @Test public void suggestsBindingInSeparateComponent() {
    JavaFileObject fooComponent = JavaFileObjects.forSourceLines("test.FooComponent",
        "package test;",
        "",
        "import dagger.Subcomponent;",
        "",
        "@Subcomponent",
        "interface FooComponent {",
        "  Foo getFoo();",
        "}");
    JavaFileObject barModule = JavaFileObjects.forSourceLines("test.BarModule",
        "package test;",
        "",
        "import dagger.Provides;",
        "import javax.inject.Inject;",
        "",
        "@dagger.Module",
        "final class BarModule {",
        "  @Provides Bar provideBar() {return null;}",
        "}");
    JavaFileObject barComponent = JavaFileObjects.forSourceLines("test.BarComponent",
        "package test;",
        "",
        "import dagger.Subcomponent;",
        "",
        "@Subcomponent(modules = {BarModule.class})",
        "interface BarComponent {",
        "  Bar getBar();",
        "}");
    JavaFileObject foo = injectable("Foo", "Bar bar");
    JavaFileObject bar = emptyInterface("Bar");

    JavaFileObject topComponent = JavaFileObjects.forSourceLines("test.TopComponent",
        "package test;",
        "",
        "import dagger.Component;",
        "",
        "@Component",
        "interface TopComponent {",
        "  FooComponent getFoo();",
        "  BarComponent getBar(BarModule barModule);",
        "}");

    Compilation compilation =
        daggerCompiler().compile(fooComponent, barComponent, topComponent, foo, bar, barModule);
    assertThat(compilation).failed();
    assertThat(compilation).hadErrorCount(1);
    assertThat(compilation)
        .hadErrorContaining("A binding with matching key exists in component: BarComponent");
  }

  @Test public void suggestsBindingInNestedSubcomponent() {
    JavaFileObject fooComponent = JavaFileObjects.forSourceLines("test.FooComponent",
        "package test;",
        "",
        "import dagger.Subcomponent;",
        "",
        "@Subcomponent",
        "interface FooComponent {",
        "  Foo getFoo();",
        "}");
    JavaFileObject barComponent = JavaFileObjects.forSourceLines("test.BarComponent",
        "package test;",
        "",
        "import dagger.Subcomponent;",
        "",
        "@Subcomponent()",
        "interface BarComponent {",
        "  BazComponent getBaz();",
        "}");
    JavaFileObject bazModule = JavaFileObjects.forSourceLines("test.BazModule",
        "package test;",
        "",
        "import dagger.Provides;",
        "import javax.inject.Inject;",
        "",
        "@dagger.Module",
        "final class BazModule {",
        "  @Provides Baz provideBaz() {return null;}",
        "}");
    JavaFileObject bazComponent = JavaFileObjects.forSourceLines("test.BazComponent",
        "package test;",
        "",
        "import dagger.Subcomponent;",
        "",
        "@Subcomponent(modules = {BazModule.class})",
        "interface BazComponent {",
        "  Baz getBaz();",
        "}");
    JavaFileObject foo = injectable("Foo", "Baz baz");
    JavaFileObject baz = emptyInterface("Baz");

    JavaFileObject topComponent = JavaFileObjects.forSourceLines("test.TopComponent",
        "package test;",
        "",
        "import dagger.Component;",
        "",
        "@Component",
        "interface TopComponent {",
        "  FooComponent getFoo();",
        "  BarComponent getBar();",
        "}");

    Compilation compilation =
        daggerCompiler()
            .compile(fooComponent, barComponent, bazComponent, topComponent, foo, baz, bazModule);
    assertThat(compilation).failed();
    assertThat(compilation).hadErrorCount(1);
    assertThat(compilation)
        .hadErrorContaining("A binding with matching key exists in component: BazComponent");
  }

  @Test
  public void missingBindingInParentComponent() {
    JavaFileObject parent =
        JavaFileObjects.forSourceLines(
            "Parent",
            "import dagger.Component;",
            "",
            "@Component",
            "interface Parent {",
            "  Foo foo();",
            "  Bar bar();",
            "  Child child();",
            "}");
    JavaFileObject child =
        JavaFileObjects.forSourceLines(
            "Child",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules=BazModule.class)",
            "interface Child {",
            "  Foo foo();",
            "  Baz baz();",
            "}");
    JavaFileObject foo =
        JavaFileObjects.forSourceLines(
            "Foo",
            "import javax.inject.Inject;",
            "",
            "class Foo {",
            "  @Inject Foo(Bar bar) {}",
            "}");
    JavaFileObject bar =
        JavaFileObjects.forSourceLines(
            "Bar",
            "import javax.inject.Inject;",
            "",
            "class Bar {",
            "  @Inject Bar(Baz baz) {}",
            "}");
    JavaFileObject baz = JavaFileObjects.forSourceLines("Baz", "class Baz {}");
    JavaFileObject bazModule = JavaFileObjects.forSourceLines(
        "BazModule",
        "import dagger.Module;",
        "import dagger.Provides;",
        "import javax.inject.Inject;",
        "",
        "@Module",
        "final class BazModule {",
        "  @Provides Baz provideBaz() {return new Baz();}",
        "}");

    Compilation compilation = daggerCompiler().compile(parent, child, foo, bar, baz, bazModule);
    assertThat(compilation).failed();
    assertThat(compilation).hadErrorCount(1);
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "\033[1;31m[Dagger/MissingBinding]\033[0m Baz cannot be provided without an "
                    + "@Inject constructor or an @Provides-annotated method.",
                "A binding with matching key exists in component: Child",
                "    Baz is injected at",
                "        Bar(baz)",
                "    Bar is requested at",
                "        Parent.bar()",
                "The following other entry points also depend on it:",
                "    Parent.foo()",
                "    Child.foo() [Parent → Child]"))
        .inFile(parent)
        .onLineContaining("interface Parent");
  }

  @Test
  public void missingBindingInSiblingComponent() {
    JavaFileObject parent =
        JavaFileObjects.forSourceLines(
            "Parent",
            "import dagger.Component;",
            "",
            "@Component",
            "interface Parent {",
            "  Foo foo();",
            "  Bar bar();",
            "  Child1 child1();",
            "  Child2 child2();",
            "}");
    JavaFileObject child1 =
        JavaFileObjects.forSourceLines(
            "Child1",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface Child1 {",
            "  Foo foo();",
            "  Baz baz();",
            "}");
    JavaFileObject child2 =
        JavaFileObjects.forSourceLines(
            "Child2",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = BazModule.class)",
            "interface Child2 {",
            "  Foo foo();",
            "  Baz baz();",
            "}");
    JavaFileObject foo =
        JavaFileObjects.forSourceLines(
            "Foo",
            "import javax.inject.Inject;",
            "",
            "class Foo {",
            "  @Inject Foo(Bar bar) {}",
            "}");
    JavaFileObject bar =
        JavaFileObjects.forSourceLines(
            "Bar",
            "import javax.inject.Inject;",
            "",
            "class Bar {",
            "  @Inject Bar(Baz baz) {}",
            "}");
    JavaFileObject baz = JavaFileObjects.forSourceLines("Baz", "class Baz {}");
    JavaFileObject bazModule = JavaFileObjects.forSourceLines(
        "BazModule",
        "import dagger.Module;",
        "import dagger.Provides;",
        "import javax.inject.Inject;",
        "",
        "@Module",
        "final class BazModule {",
        "  @Provides Baz provideBaz() {return new Baz();}",
        "}");

    Compilation compilation =
        daggerCompiler().compile(parent, child1, child2, foo, bar, baz, bazModule);
    assertThat(compilation).failed();
    assertThat(compilation).hadErrorCount(1);
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "\033[1;31m[Dagger/MissingBinding]\033[0m Baz cannot be provided without an "
                    + "@Inject constructor or an @Provides-annotated method.",
                "A binding with matching key exists in component: Child2",
                "    Baz is injected at",
                "        Bar(baz)",
                "    Bar is requested at",
                "        Parent.bar()",
                "The following other entry points also depend on it:",
                "    Parent.foo()",
                "    Child1.foo() [Parent → Child1]",
                "    Child2.foo() [Parent → Child2]",
                "    Child1.baz() [Parent → Child1]"))
        .inFile(parent)
        .onLineContaining("interface Parent");
  }
}
