/*
 * 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.internal.codegen;

import static com.google.testing.compile.CompilationSubject.assertThat;
import static dagger.internal.codegen.Compilers.compilerWithOptions;
import static dagger.internal.codegen.Compilers.daggerCompiler;
import static dagger.internal.codegen.TestUtils.message;
import static org.junit.Assume.assumeFalse;

import com.google.common.collect.ImmutableList;
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.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class DuplicateBindingsValidationTest {

  @Parameters(name = "fullBindingGraphValidation={0}")
  public static ImmutableList<Object[]> parameters() {
    return ImmutableList.copyOf(new Object[][] {{false}, {true}});
  }

  private final boolean fullBindingGraphValidation;

  public DuplicateBindingsValidationTest(boolean fullBindingGraphValidation) {
    this.fullBindingGraphValidation = fullBindingGraphValidation;
  }

  @Test public void duplicateExplicitBindings_ProvidesAndComponentProvision() {
    assumeFalse(fullBindingGraphValidation);

    JavaFileObject component = JavaFileObjects.forSourceLines("test.Outer",
        "package test;",
        "",
        "import dagger.Component;",
        "import dagger.Module;",
        "import dagger.Provides;",
        "",
        "final class Outer {",
        "  interface A {}",
        "",
        "  interface B {}",
        "",
        "  @Module",
        "  static class AModule {",
        "    @Provides String provideString() { return \"\"; }",
        "    @Provides A provideA(String s) { return new A() {}; }",
        "  }",
        "",
        "  @Component(modules = AModule.class)",
        "  interface Parent {",
        "    A getA();",
        "  }",
        "",
        "  @Module",
        "  static class BModule {",
        "    @Provides B provideB(A a) { return new B() {}; }",
        "  }",
        "",
        "  @Component(dependencies = Parent.class, modules = { BModule.class, AModule.class})",
        "  interface Child {",
        "    B getB();",
        "  }",
        "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Outer.A is bound multiple times:",
                "    @Provides Outer.A Outer.AModule.provideA(String)",
                "    Outer.A Outer.Parent.getA()"))
        .inFile(component)
        .onLineContaining("interface Child");
  }

  @Test public void duplicateExplicitBindings_TwoProvidesMethods() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import javax.inject.Inject;",
            "",
            "final class Outer {",
            "  interface A {}",
            "",
            "  static class B {",
            "    @Inject B(A a) {}",
            "  }",
            "",
            "  @Module",
            "  static class Module1 {",
            "    @Provides A provideA1() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module2 {",
            "    @Provides String provideString() { return \"\"; }",
            "    @Provides A provideA2(String s) { return new A() {}; }",
            "  }",
            "",
            "  @Module(includes = { Module1.class, Module2.class})",
            "  abstract static class Module3 {}",
            "",
            "  @Component(modules = { Module1.class, Module2.class})",
            "  interface TestComponent {",
            "    A getA();",
            "    B getB();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Outer.A is bound multiple times:",
                "    @Provides Outer.A Outer.Module1.provideA1()",
                "    @Provides Outer.A Outer.Module2.provideA2(String)"))
        .inFile(component)
        .onLineContaining("interface TestComponent");

    if (fullBindingGraphValidation) {
      assertThat(compilation)
          .hadErrorContaining(
              message(
                  "Outer.A is bound multiple times:",
                  "    @Provides Outer.A Outer.Module1.provideA1()",
                  "    @Provides Outer.A Outer.Module2.provideA2(String)"))
          .inFile(component)
          .onLineContaining("class Module3");
    }

    // The duplicate bindngs are also requested from B, but we don't want to report them again.
    assertThat(compilation).hadErrorCount(fullBindingGraphValidation ? 2 : 1);
  }

  @Test
  public void duplicateExplicitBindings_ProvidesVsBinds() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Binds;",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import javax.inject.Inject;",
            "",
            "final class Outer {",
            "  interface A {}",
            "",
            "  static final class B implements A {",
            "    @Inject B() {}",
            "  }",
            "",
            "  @Module",
            "  static class Module1 {",
            "    @Provides A provideA1() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static abstract class Module2 {",
            "    @Binds abstract A bindA2(B b);",
            "  }",
            "",
            "  @Module(includes = { Module1.class, Module2.class})",
            "  abstract static class Module3 {}",
            "",
            "  @Component(modules = { Module1.class, Module2.class})",
            "  interface TestComponent {",
            "    A getA();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Outer.A is bound multiple times:",
                "    @Provides Outer.A Outer.Module1.provideA1()",
                "    @Binds Outer.A Outer.Module2.bindA2(Outer.B)"))
        .inFile(component)
        .onLineContaining("interface TestComponent");

    if (fullBindingGraphValidation) {
      assertThat(compilation)
          .hadErrorContaining(
              message(
                  "Outer.A is bound multiple times:",
                  "    @Provides Outer.A Outer.Module1.provideA1()",
                  "    @Binds Outer.A Outer.Module2.bindA2(Outer.B)"))
          .inFile(component)
          .onLineContaining("class Module3");
    }
  }

  @Test
  public void duplicateExplicitBindings_multibindingsAndExplicitSets() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Binds;",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.multibindings.IntoSet;",
            "import java.util.HashSet;",
            "import java.util.Set;",
            "import javax.inject.Qualifier;",
            "",
            "final class Outer {",
            "  @Qualifier @interface SomeQualifier {}",
            "",
            "  @Module",
            "  abstract static class TestModule1 {",
            "    @Provides @IntoSet static String stringSetElement() { return \"\"; }",
            "",
            "    @Binds",
            "    @IntoSet abstract String bindStringSetElement(@SomeQualifier String value);",
            "",
            "    @Provides @SomeQualifier",
            "    static String provideSomeQualifiedString() { return \"\"; }",
            "  }",
            "",
            "  @Module",
            "  static class TestModule2 {",
            "    @Provides Set<String> stringSet() { return new HashSet<String>(); }",
            "  }",
            "",
            "  @Module(includes = { TestModule1.class, TestModule2.class})",
            "  abstract static class TestModule3 {}",
            "",
            "  @Component(modules = { TestModule1.class, TestModule2.class })",
            "  interface TestComponent {",
            "    Set<String> getStringSet();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Set<String> has incompatible bindings or declarations:",
                "    Set bindings and declarations:",
                "        @Binds @IntoSet String "
                    + "Outer.TestModule1.bindStringSetElement(@Outer.SomeQualifier "
                    + "String)",
                "        @Provides @IntoSet String "
                    + "Outer.TestModule1.stringSetElement()",
                "    Unique bindings and declarations:",
                "        @Provides Set<String> Outer.TestModule2.stringSet()"))
        .inFile(component)
        .onLineContaining(
            fullBindingGraphValidation ? "class TestModule3" : "interface TestComponent");
  }

  @Test
  public void duplicateExplicitBindings_multibindingsAndExplicitMaps() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Binds;",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.multibindings.IntoMap;",
            "import dagger.multibindings.StringKey;",
            "import java.util.HashMap;",
            "import java.util.Map;",
            "import javax.inject.Qualifier;",
            "",
            "final class Outer {",
            "  @Qualifier @interface SomeQualifier {}",
            "",
            "  @Module",
            "  abstract static class TestModule1 {",
            "    @Provides @IntoMap",
            "    @StringKey(\"foo\")",
            "    static String stringMapEntry() { return \"\"; }",
            "",
            "    @Binds @IntoMap @StringKey(\"bar\")",
            "    abstract String bindStringMapEntry(@SomeQualifier String value);",
            "",
            "    @Provides @SomeQualifier",
            "    static String provideSomeQualifiedString() { return \"\"; }",
            "  }",
            "",
            "  @Module",
            "  static class TestModule2 {",
            "    @Provides Map<String, String> stringMap() {",
            "      return new HashMap<String, String>();",
            "    }",
            "  }",
            "",
            "  @Module(includes = { TestModule1.class, TestModule2.class})",
            "  abstract static class TestModule3 {}",
            "",
            "  @Component(modules = { TestModule1.class, TestModule2.class })",
            "  interface TestComponent {",
            "    Map<String, String> getStringMap();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Map<String,String> has incompatible bindings "
                    + "or declarations:",
                "    Map bindings and declarations:",
                "        @Binds @IntoMap @StringKey(\"bar\") String"
                    + " Outer.TestModule1.bindStringMapEntry(@Outer.SomeQualifier "
                    + "String)",
                "        @Provides @IntoMap @StringKey(\"foo\") String"
                    + " Outer.TestModule1.stringMapEntry()",
                "    Unique bindings and declarations:",
                "        @Provides Map<String,String> Outer.TestModule2.stringMap()"))
        .inFile(component)
        .onLineContaining(
            fullBindingGraphValidation ? "class TestModule3" : "interface TestComponent");
  }

  @Test
  public void duplicateExplicitBindings_UniqueBindingAndMultibindingDeclaration_Set() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.multibindings.Multibinds;",
            "import java.util.HashSet;",
            "import java.util.Set;",
            "",
            "final class Outer {",
            "  @Module",
            "  abstract static class TestModule1 {",
            "    @Multibinds abstract Set<String> stringSet();",
            "  }",
            "",
            "  @Module",
            "  static class TestModule2 {",
            "    @Provides Set<String> stringSet() { return new HashSet<String>(); }",
            "  }",
            "",
            "  @Module(includes = { TestModule1.class, TestModule2.class})",
            "  abstract static class TestModule3 {}",
            "",
            "  @Component(modules = { TestModule1.class, TestModule2.class })",
            "  interface TestComponent {",
            "    Set<String> getStringSet();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Set<String> has incompatible bindings or declarations:",
                "    Set bindings and declarations:",
                "        @Multibinds Set<String> "
                    + "Outer.TestModule1.stringSet()",
                "    Unique bindings and declarations:",
                "        @Provides Set<String> Outer.TestModule2.stringSet()"))
        .inFile(component)
        .onLineContaining(
            fullBindingGraphValidation ? "class TestModule3" : "interface TestComponent");
  }

  @Test
  public void duplicateExplicitBindings_UniqueBindingAndMultibindingDeclaration_Map() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.multibindings.Multibinds;",
            "import java.util.HashMap;",
            "import java.util.Map;",
            "",
            "final class Outer {",
            "  @Module",
            "  abstract static class TestModule1 {",
            "    @Multibinds abstract Map<String, String> stringMap();",
            "  }",
            "",
            "  @Module",
            "  static class TestModule2 {",
            "    @Provides Map<String, String> stringMap() {",
            "      return new HashMap<String, String>();",
            "    }",
            "  }",
            "",
            "  @Module(includes = { TestModule1.class, TestModule2.class})",
            "  abstract static class TestModule3 {}",
            "",
            "  @Component(modules = { TestModule1.class, TestModule2.class })",
            "  interface TestComponent {",
            "    Map<String, String> getStringMap();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Map<String,String> has incompatible bindings "
                    + "or declarations:",
                "    Map bindings and declarations:",
                "        @Multibinds Map<String,String> "
                    + "Outer.TestModule1.stringMap()",
                "    Unique bindings and declarations:",
                "        @Provides Map<String,String> Outer.TestModule2.stringMap()"))
        .inFile(component)
        .onLineContaining(
            fullBindingGraphValidation ? "class TestModule3" : "interface TestComponent");
  }

  @Test public void duplicateBindings_TruncateAfterLimit() {
    JavaFileObject component =
        JavaFileObjects.forSourceLines(
            "test.Outer",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import javax.inject.Inject;",
            "",
            "final class Outer {",
            "  interface A {}",
            "",
            "  @Module",
            "  static class Module01 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module02 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module03 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module04 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module05 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module06 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module07 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module08 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module09 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module10 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module11 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module",
            "  static class Module12 {",
            "    @Provides A provideA() { return new A() {}; }",
            "  }",
            "",
            "  @Module(includes = {",
            "    Module01.class,",
            "    Module02.class,",
            "    Module03.class,",
            "    Module04.class,",
            "    Module05.class,",
            "    Module06.class,",
            "    Module07.class,",
            "    Module08.class,",
            "    Module09.class,",
            "    Module10.class,",
            "    Module11.class,",
            "    Module12.class",
            "  })",
            "  abstract static class Modules {}",
            "",
            "  @Component(modules = {",
            "    Module01.class,",
            "    Module02.class,",
            "    Module03.class,",
            "    Module04.class,",
            "    Module05.class,",
            "    Module06.class,",
            "    Module07.class,",
            "    Module08.class,",
            "    Module09.class,",
            "    Module10.class,",
            "    Module11.class,",
            "    Module12.class",
            "  })",
            "  interface TestComponent {",
            "    A getA();",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(component);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Outer.A is bound multiple times:",
                "    @Provides Outer.A Outer.Module01.provideA()",
                "    @Provides Outer.A Outer.Module02.provideA()",
                "    @Provides Outer.A Outer.Module03.provideA()",
                "    @Provides Outer.A Outer.Module04.provideA()",
                "    @Provides Outer.A Outer.Module05.provideA()",
                "    @Provides Outer.A Outer.Module06.provideA()",
                "    @Provides Outer.A Outer.Module07.provideA()",
                "    @Provides Outer.A Outer.Module08.provideA()",
                "    @Provides Outer.A Outer.Module09.provideA()",
                "    @Provides Outer.A Outer.Module10.provideA()",
                "    and 2 others"))
        .inFile(component)
        .onLineContaining(fullBindingGraphValidation ? "class Modules" : "interface TestComponent");
  }

  @Test
  public void childBindingConflictsWithParent() {
    JavaFileObject aComponent =
        JavaFileObjects.forSourceLines(
            "test.A",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "",
            "@Component(modules = A.AModule.class)",
            "interface A {",
            "  Object conflict();",
            "",
            "  B.Builder b();",
            "",
            "  @Module(subcomponents = B.class)",
            "  static class AModule {",
            "    @Provides static Object abConflict() {",
            "      return \"a\";",
            "    }",
            "  }",
            "}");
    JavaFileObject bComponent =
        JavaFileObjects.forSourceLines(
            "test.B",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = B.BModule.class)",
            "interface B {",
            "  Object conflict();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    B build();",
            "  }",
            "",
            "  @Module",
            "  static class BModule {",
            "    @Provides static Object abConflict() {",
            "      return \"b\";",
            "    }",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(aComponent, bComponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Object is bound multiple times:",
                "    @Provides Object test.A.AModule.abConflict()",
                "    @Provides Object test.B.BModule.abConflict()"))
        .inFile(aComponent)
        .onLineContaining(fullBindingGraphValidation ? "class AModule" : "interface A {");
  }

  @Test
  public void grandchildBindingConflictsWithGrandparent() {
    JavaFileObject aComponent =
        JavaFileObjects.forSourceLines(
            "test.A",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "",
            "@Component(modules = A.AModule.class)",
            "interface A {",
            "  Object conflict();",
            "",
            "  B.Builder b();",
            "",
            "  @Module(subcomponents = B.class)",
            "  static class AModule {",
            "    @Provides static Object acConflict() {",
            "      return \"a\";",
            "    }",
            "  }",
            "}");
    JavaFileObject bComponent =
        JavaFileObjects.forSourceLines(
            "test.B",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface B {",
            "  C.Builder c();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    B build();",
            "  }",
            "}");
    JavaFileObject cComponent =
        JavaFileObjects.forSourceLines(
            "test.C",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = C.CModule.class)",
            "interface C {",
            "  Object conflict();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    C build();",
            "  }",
            "",
            "  @Module",
            "  static class CModule {",
            "    @Provides static Object acConflict() {",
            "      return \"c\";",
            "    }",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(aComponent, bComponent, cComponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Object is bound multiple times:",
                "    @Provides Object test.A.AModule.acConflict()",
                "    @Provides Object test.C.CModule.acConflict()"))
        .inFile(aComponent)
        .onLineContaining(fullBindingGraphValidation ? "class AModule" : "interface A {");
  }

  @Test
  public void grandchildBindingConflictsWithChild() {
    JavaFileObject aComponent =
        JavaFileObjects.forSourceLines(
            "test.A",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface A {",
            "  B b();",
            "}");
    JavaFileObject bComponent =
        JavaFileObjects.forSourceLines(
            "test.B",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = B.BModule.class)",
            "interface B {",
            "  Object conflict();",
            "",
            "  C.Builder c();",
            "",
            "  @Module(subcomponents = C.class)",
            "  static class BModule {",
            "    @Provides static Object bcConflict() {",
            "      return \"b\";",
            "    }",
            "  }",
            "}");
    JavaFileObject cComponent =
        JavaFileObjects.forSourceLines(
            "test.C",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = C.CModule.class)",
            "interface C {",
            "  Object conflict();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    C build();",
            "  }",
            "",
            "  @Module",
            "  static class CModule {",
            "    @Provides static Object bcConflict() {",
            "      return \"c\";",
            "    }",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                fullBindingGraphValidationOption())
            .compile(aComponent, bComponent, cComponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Object is bound multiple times:",
                "    @Provides Object test.B.BModule.bcConflict()",
                "    @Provides Object test.C.CModule.bcConflict()"))
        .inFile(fullBindingGraphValidation ? bComponent : aComponent)
        .onLineContaining(fullBindingGraphValidation ? "class BModule" : "interface A {");
  }

  @Test
  public void childProvidesConflictsWithParentInjects() {
    assumeFalse(fullBindingGraphValidation);

    JavaFileObject foo =
        JavaFileObjects.forSourceLines(
            "test.Foo",
            "package test;",
            "",
            "import java.util.Set;",
            "import javax.inject.Inject;",
            "",
            "final class Foo {",
            "  @Inject Foo(Set<String> strings) {}",
            "}");
    JavaFileObject injected1 =
        JavaFileObjects.forSourceLines(
            "test.Injected1",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.multibindings.IntoSet;",
            "import java.util.Set;",
            "",
            "@Component(modules = Injected1.Injected1Module.class)",
            "interface Injected1 {",
            "  Foo foo();",
            "  Injected2 injected2();",
            "",
            "  @Module",
            "  interface Injected1Module {",
            "    @Provides @IntoSet static String string() {",
            "      return \"injected1\";",
            "    }",
            "  }",
            "}");
    JavaFileObject injected2 =
        JavaFileObjects.forSourceLines(
            "test.Injected2",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "import dagger.multibindings.IntoSet;",
            "import java.util.Set;",
            "",
            "@Subcomponent(modules = Injected2.Injected2Module.class)",
            "interface Injected2 {",
            "  Foo foo();",
            "  Provided1 provided1();",
            "",
            "  @Module",
            "  interface Injected2Module {",
            "    @Provides @IntoSet static String string() {",
            "      return \"injected2\";",
            "    }",
            "  }",
            "}");
    JavaFileObject provided1 =
        JavaFileObjects.forSourceLines(
            "test.Provided1",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "import dagger.multibindings.IntoSet;",
            "import java.util.Set;",
            "",
            "@Subcomponent(modules = Provided1.Provided1Module.class)",
            "interface Provided1 {",
            "  Foo foo();",
            "  Provided2 provided2();",
            "",
            "  @Module",
            "  static class Provided1Module {",
            "    @Provides static Foo provideFoo(Set<String> strings) {",
            "      return new Foo(strings);",
            "    }",
            "",
            "    @Provides @IntoSet static String string() {",
            "      return \"provided1\";",
            "    }",
            "  }",
            "}");
    JavaFileObject provided2 =
        JavaFileObjects.forSourceLines(
            "test.Provided2",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "import dagger.multibindings.IntoSet;",
            "",
            "@Subcomponent(modules = Provided2.Provided2Module.class)",
            "interface Provided2 {",
            "  Foo foo();",
            "",
            "  @Module",
            "  static class Provided2Module {",
            "    @Provides @IntoSet static String string() {",
            "      return \"provided2\";",
            "    }",
            "  }",
            "}");

    Compilation compilation =
        daggerCompiler().compile(foo, injected1, injected2, provided1, provided2);
    assertThat(compilation).succeeded();
    assertThat(compilation)
        .hadWarningContaining(
            message(
                "Foo is bound multiple times:",
                "    @Inject Foo(Set<String>) [Injected1]",
                "    @Provides Foo Provided1.Provided1Module.provideFoo(Set<String>) "
                    + "[Injected1 → Injected2 → Provided1]"))
        .inFile(injected1)
        .onLineContaining("interface Injected1 {");
  }

  @Test
  public void grandchildBindingConflictsWithParentWithNullableViolationAsWarning() {
    JavaFileObject parentConflictsWithChild =
        JavaFileObjects.forSourceLines(
            "test.ParentConflictsWithChild",
            "package test;",
            "",
            "import dagger.Component;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import javax.annotation.Nullable;",
            "",
            "@Component(modules = ParentConflictsWithChild.ParentModule.class)",
            "interface ParentConflictsWithChild {",
            "  Child.Builder child();",
            "",
            "  @Module(subcomponents = Child.class)",
            "  static class ParentModule {",
            "    @Provides @Nullable static Object nullableParentChildConflict() {",
            "      return \"parent\";",
            "    }",
            "  }",
            "}");
    JavaFileObject child =
        JavaFileObjects.forSourceLines(
            "test.Child",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = Child.ChildModule.class)",
            "interface Child {",
            "  Object parentChildConflictThatViolatesNullability();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    Child build();",
            "  }",
            "",
            "  @Module",
            "  static class ChildModule {",
            "    @Provides static Object nonNullableParentChildConflict() {",
            "      return \"child\";",
            "    }",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(
                "-Adagger.nullableValidation=WARNING",
                fullBindingGraphValidationOption())
            .compile(parentConflictsWithChild, child);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            message(
                "Object is bound multiple times:",
                "    @Provides Object Child.ChildModule.nonNullableParentChildConflict()",
                "    @Provides @Nullable Object"
                    + " ParentConflictsWithChild.ParentModule.nullableParentChildConflict()"))
        .inFile(parentConflictsWithChild)
        .onLineContaining(
            fullBindingGraphValidation
                ? "class ParentModule"
                : "interface ParentConflictsWithChild");
  }

  private String fullBindingGraphValidationOption() {
    return "-Adagger.fullBindingGraphValidation=" + (fullBindingGraphValidation ? "ERROR" : "NONE");
  }

  @Test
  public void reportedInParentAndChild() {
    JavaFileObject parent =
        JavaFileObjects.forSourceLines(
            "test.Parent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component(modules = ParentModule.class)",
            "interface Parent {",
            "  Child.Builder childBuilder();",
            "  String duplicated();",
            "}");
    JavaFileObject parentModule =
        JavaFileObjects.forSourceLines(
            "test.ParentModule",
            "package test;",
            "",
            "import dagger.BindsOptionalOf;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import java.util.Optional;",
            "",
            "@Module",
            "interface ParentModule {",
            "  @Provides static String one(Optional<Object> optional) { return \"one\"; }",
            "  @Provides static String two() { return \"two\"; }",
            "  @BindsOptionalOf Object optional();",
            "}");
    JavaFileObject child =
        JavaFileObjects.forSourceLines(
            "test.Child",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent(modules = ChildModule.class)",
            "interface Child {",
            "  String duplicated();",
            "",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    Child build();",
            "  }",
            "}");
    JavaFileObject childModule =
        JavaFileObjects.forSourceLines(
            "test.ChildModule",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import java.util.Optional;",
            "",
            "@Module",
            "interface ChildModule {",
            "  @Provides static Object object() { return \"object\"; }",
            "}");
    Compilation compilation = daggerCompiler().compile(parent, parentModule, child, childModule);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining("String is bound multiple times")
        .inFile(parent)
        .onLineContaining("interface Parent");
    assertThat(compilation).hadErrorCount(1);
  }
}
