/*
 * Copyright (C) 2010 Google Inc.
 *
 * 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 com.google.inject;

import static com.google.inject.Asserts.*;
import static com.google.inject.name.Names.named;

import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.inject.name.Named;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;
import com.google.inject.util.Providers;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.logging.Logger;
import junit.framework.TestCase;

/**
 * A suite of tests for duplicate bindings.
 *
 * @author sameb@google.com (Sam Berlin)
 */
public class DuplicateBindingsTest extends TestCase {

  private FooImpl foo = new FooImpl();
  private Provider<Foo> pFoo = Providers.<Foo>of(new FooImpl());
  private Class<? extends Provider<? extends Foo>> pclFoo = FooProvider.class;
  private Class<? extends Foo> clFoo = FooImpl.class;
  private Constructor<FooImpl> cFoo = FooImpl.cxtor();

  public void testDuplicateBindingsAreIgnored() {
    Injector injector =
        Guice.createInjector(
            new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo),
            new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo));
    List<Key<?>> bindings = Lists.newArrayList(injector.getAllBindings().keySet());
    removeBasicBindings(bindings);

    // Ensure only one binding existed for each type.
    assertTrue(bindings.remove(Key.get(Foo.class, named("instance"))));
    assertTrue(bindings.remove(Key.get(Foo.class, named("pInstance"))));
    assertTrue(bindings.remove(Key.get(Foo.class, named("pKey"))));
    assertTrue(bindings.remove(Key.get(Foo.class, named("linkedKey"))));
    assertTrue(bindings.remove(Key.get(FooImpl.class)));
    assertTrue(bindings.remove(Key.get(Foo.class, named("constructor"))));
    assertTrue(bindings.remove(Key.get(FooProvider.class))); // JIT binding
    assertTrue(bindings.remove(Key.get(Foo.class, named("providerMethod"))));

    assertEquals(bindings.toString(), 0, bindings.size());
  }

  public void testElementsDeduplicate() {
    List<Element> elements =
        Elements.getElements(
            new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo),
            new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo));
    assertEquals(14, elements.size());
    assertEquals(7, new LinkedHashSet<Element>(elements).size());
  }

  public void testProviderMethodsFailIfInstancesDiffer() {
    try {
      Guice.createInjector(new FailingProviderModule(), new FailingProviderModule());
      fail("should have failed");
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "A binding to "
              + Foo.class.getName()
              + " was already configured "
              + "at "
              + FailingProviderModule.class.getName(),
          "at " + FailingProviderModule.class.getName());
    }
  }

  public void testSameScopeInstanceIgnored() {
    Guice.createInjector(
        new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo),
        new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo));

    Guice.createInjector(
        new ScopedModule(Scopes.NO_SCOPE, foo, pFoo, pclFoo, clFoo, cFoo),
        new ScopedModule(Scopes.NO_SCOPE, foo, pFoo, pclFoo, clFoo, cFoo));
  }

  public void testSameScopeAnnotationIgnored() {
    Guice.createInjector(
        new AnnotatedScopeModule(Singleton.class, foo, pFoo, pclFoo, clFoo, cFoo),
        new AnnotatedScopeModule(Singleton.class, foo, pFoo, pclFoo, clFoo, cFoo));
  }

  public void testMixedAnnotationAndScopeForSingletonIgnored() {
    Guice.createInjector(
        new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo),
        new AnnotatedScopeModule(Singleton.class, foo, pFoo, pclFoo, clFoo, cFoo));
  }

  public void testMixedScopeAndUnscopedIgnored() {
    Guice.createInjector(
        new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo),
        new ScopedModule(Scopes.NO_SCOPE, foo, pFoo, pclFoo, clFoo, cFoo));
  }

  public void testMixedScopeFails() {
    try {
      Guice.createInjector(
          new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo),
          new ScopedModule(Scopes.SINGLETON, foo, pFoo, pclFoo, clFoo, cFoo));
      fail("expected exception");
    } catch (CreationException ce) {
      String segment1 =
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("pInstance")
              + " was already configured at "
              + SimpleModule.class.getName();
      String segment2 =
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("pKey")
              + " was already configured at "
              + SimpleModule.class.getName();
      String segment3 =
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("constructor")
              + " was already configured at "
              + SimpleModule.class.getName();
      String segment4 =
          "A binding to "
              + FooImpl.class.getName()
              + " was already configured at "
              + SimpleModule.class.getName();
      String atSegment = "at " + ScopedModule.class.getName();
      if (isIncludeStackTraceOff()) {
        assertContains(
            ce.getMessage(),
            segment1,
            atSegment,
            segment2,
            atSegment,
            segment3,
            atSegment,
            segment4,
            atSegment);
      } else {
        assertContains(
            ce.getMessage(),
            segment1,
            atSegment,
            segment2,
            atSegment,
            segment4,
            atSegment,
            segment3,
            atSegment);
      }
    }
  }

  @SuppressWarnings("unchecked")
  public void testMixedTargetsFails() {
    try {
      Guice.createInjector(
          new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo),
          new SimpleModule(
              new FooImpl(),
              Providers.<Foo>of(new FooImpl()),
              (Class) BarProvider.class,
              (Class) Bar.class,
              (Constructor) Bar.cxtor()));
      fail("expected exception");
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("pInstance")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("pKey")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("linkedKey")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("constructor")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName());
    }
  }

  public void testExceptionInEqualsThrowsCreationException() {
    try {
      Guice.createInjector(new ThrowingModule(), new ThrowingModule());
      fail("expected exception");
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "A binding to "
              + Foo.class.getName()
              + " was already configured at "
              + ThrowingModule.class.getName(),
          "and an error was thrown while checking duplicate bindings.  Error: java.lang.RuntimeException: Boo!",
          "at " + ThrowingModule.class.getName());
    }
  }

  public void testChildInjectorDuplicateParentFail() {
    Injector injector = Guice.createInjector(new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo));

    try {
      injector.createChildInjector(new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo));
      fail("expected exception");
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("pInstance")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("pKey")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("linkedKey")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("constructor")
              + " was already configured at "
              + SimpleModule.class.getName(),
          "at " + SimpleModule.class.getName(),
          "A binding to "
              + Foo.class.getName()
              + " annotated with "
              + named("providerMethod")
              + " was already configured at "
              + SimpleProviderModule.class.getName(),
          "at " + SimpleProviderModule.class.getName());
    }
  }

  public void testDuplicatesSolelyInChildIgnored() {
    Injector injector = Guice.createInjector();
    injector.createChildInjector(
        new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo),
        new SimpleModule(foo, pFoo, pclFoo, clFoo, cFoo));
  }

  public void testDifferentBindingTypesFail() {
    List<Element> elements = Elements.getElements(new FailedModule(foo, pFoo, pclFoo, clFoo, cFoo));

    // Make sure every combination of the elements with another element fails.
    // This ensures that duplication checks the kind of binding also.
    for (Element e1 : elements) {
      for (Element e2 : elements) {
        // if they're the same, this shouldn't fail.
        try {
          Guice.createInjector(Elements.getModule(Arrays.asList(e1, e2)));
          if (e1 != e2) {
            fail("must fail!");
          }
        } catch (CreationException expected) {
          if (e1 != e2) {
            assertContains(
                expected.getMessage(),
                "A binding to "
                    + Foo.class.getName()
                    + " was already configured at "
                    + FailedModule.class.getName(),
                "at " + FailedModule.class.getName());
          } else {
            throw expected;
          }
        }
      }
    }
  }

  public void testJitBindingsAreCheckedAfterConversions() {
    Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(A.class);
            bind(A.class).to(RealA.class);
          }
        });
  }

  public void testEqualsNotCalledByDefaultOnInstance() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnEquals = true;
    Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class);
            bind(HashEqualsTester.class).toInstance(a);
          }
        });
  }

  public void testEqualsNotCalledByDefaultOnProvider() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnEquals = true;
    Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class);
            bind(Object.class).toProvider(a);
          }
        });
  }

  public void testHashcodeNeverCalledOnInstance() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnHashcode = true;
    a.equality = "test";

    final HashEqualsTester b = new HashEqualsTester();
    b.throwOnHashcode = true;
    b.equality = "test";
    Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class);
            bind(HashEqualsTester.class).toInstance(a);
            bind(HashEqualsTester.class).toInstance(b);
          }
        });
  }

  public void testHashcodeNeverCalledOnProviderInstance() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnHashcode = true;
    a.equality = "test";

    final HashEqualsTester b = new HashEqualsTester();
    b.throwOnHashcode = true;
    b.equality = "test";
    Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class);
            bind(Object.class).toProvider(a);
            bind(Object.class).toProvider(b);
          }
        });
  }

  private static class RealA extends A {}

  @ImplementedBy(RealA.class)
  private static class A {}

  private void removeBasicBindings(Collection<Key<?>> bindings) {
    bindings.remove(Key.get(Injector.class));
    bindings.remove(Key.get(Logger.class));
    bindings.remove(Key.get(Stage.class));
  }

  private static class ThrowingModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(Foo.class)
          .toInstance(
              new Foo() {
                @Override
                public boolean equals(Object obj) {
                  throw new RuntimeException("Boo!");
                }

                @Override
                public int hashCode() {
                  throw new RuntimeException("Boo!");
                }
              });
    }
  }

  private abstract static class FooModule extends AbstractModule {
    protected final FooImpl foo;
    protected final Provider<Foo> pFoo;
    protected final Class<? extends Provider<? extends Foo>> pclFoo;
    protected final Class<? extends Foo> clFoo;
    protected final Constructor<FooImpl> cFoo;

    FooModule(
        FooImpl foo,
        Provider<Foo> pFoo,
        Class<? extends Provider<? extends Foo>> pclFoo,
        Class<? extends Foo> clFoo,
        Constructor<FooImpl> cFoo) {
      this.foo = foo;
      this.pFoo = pFoo;
      this.pclFoo = pclFoo;
      this.clFoo = clFoo;
      this.cFoo = cFoo;
    }
  }

  private static class FailedModule extends FooModule {
    FailedModule(
        FooImpl foo,
        Provider<Foo> pFoo,
        Class<? extends Provider<? extends Foo>> pclFoo,
        Class<? extends Foo> clFoo,
        Constructor<FooImpl> cFoo) {
      super(foo, pFoo, pclFoo, clFoo, cFoo);
    }

    @Override
    protected void configure() {
      // InstanceBinding
      bind(Foo.class).toInstance(foo);

      // ProviderInstanceBinding
      bind(Foo.class).toProvider(pFoo);

      // ProviderKeyBinding
      bind(Foo.class).toProvider(pclFoo);

      // LinkedKeyBinding
      bind(Foo.class).to(clFoo);

      // ConstructorBinding
      bind(Foo.class).toConstructor(cFoo);
    }

    @Provides
    Foo foo() {
      return null;
    }
  }

  private static class FailingProviderModule extends AbstractModule {

    @Provides
    Foo foo() {
      return null;
    }
  }

  private static class SimpleProviderModule extends AbstractModule {

    @Provides
    @Named("providerMethod")
    Foo foo() {
      return null;
    }

    @Override
    @SuppressWarnings({"EqualsHashCode", "EqualsBrokenForNull"}) // intentionally NPE on null for the test
    public boolean equals(Object obj) {
      return obj.getClass() == getClass();
    }
  }

  private static class SimpleModule extends FooModule {
    SimpleModule(
        FooImpl foo,
        Provider<Foo> pFoo,
        Class<? extends Provider<? extends Foo>> pclFoo,
        Class<? extends Foo> clFoo,
        Constructor<FooImpl> cFoo) {
      super(foo, pFoo, pclFoo, clFoo, cFoo);
    }

    @Override
    protected void configure() {
      // InstanceBinding
      bind(Foo.class).annotatedWith(named("instance")).toInstance(foo);

      // ProviderInstanceBinding
      bind(Foo.class).annotatedWith(named("pInstance")).toProvider(pFoo);

      // ProviderKeyBinding
      bind(Foo.class).annotatedWith(named("pKey")).toProvider(pclFoo);

      // LinkedKeyBinding
      bind(Foo.class).annotatedWith(named("linkedKey")).to(clFoo);

      // UntargettedBinding / ConstructorBinding
      bind(FooImpl.class);

      // ConstructorBinding
      bind(Foo.class).annotatedWith(named("constructor")).toConstructor(cFoo);

      // ProviderMethod
      // (reconstructed from an Element to ensure it doesn't get filtered out
      //  by deduplicating Modules)
      install(Elements.getModule(Elements.getElements(new SimpleProviderModule())));
    }
  }

  private static class ScopedModule extends FooModule {
    private final Scope scope;

    ScopedModule(
        Scope scope,
        FooImpl foo,
        Provider<Foo> pFoo,
        Class<? extends Provider<? extends Foo>> pclFoo,
        Class<? extends Foo> clFoo,
        Constructor<FooImpl> cFoo) {
      super(foo, pFoo, pclFoo, clFoo, cFoo);
      this.scope = scope;
    }

    @Override
    protected void configure() {
      // ProviderInstanceBinding
      bind(Foo.class).annotatedWith(named("pInstance")).toProvider(pFoo).in(scope);

      // ProviderKeyBinding
      bind(Foo.class).annotatedWith(named("pKey")).toProvider(pclFoo).in(scope);

      // LinkedKeyBinding
      bind(Foo.class).annotatedWith(named("linkedKey")).to(clFoo).in(scope);

      // UntargettedBinding / ConstructorBinding
      bind(FooImpl.class).in(scope);

      // ConstructorBinding
      bind(Foo.class).annotatedWith(named("constructor")).toConstructor(cFoo).in(scope);
    }
  }

  private static class AnnotatedScopeModule extends FooModule {
    private final Class<? extends Annotation> scope;

    AnnotatedScopeModule(
        Class<? extends Annotation> scope,
        FooImpl foo,
        Provider<Foo> pFoo,
        Class<? extends Provider<? extends Foo>> pclFoo,
        Class<? extends Foo> clFoo,
        Constructor<FooImpl> cFoo) {
      super(foo, pFoo, pclFoo, clFoo, cFoo);
      this.scope = scope;
    }

    @Override
    protected void configure() {
      // ProviderInstanceBinding
      bind(Foo.class).annotatedWith(named("pInstance")).toProvider(pFoo).in(scope);

      // ProviderKeyBinding
      bind(Foo.class).annotatedWith(named("pKey")).toProvider(pclFoo).in(scope);

      // LinkedKeyBinding
      bind(Foo.class).annotatedWith(named("linkedKey")).to(clFoo).in(scope);

      // UntargettedBinding / ConstructorBinding
      bind(FooImpl.class).in(scope);

      // ConstructorBinding
      bind(Foo.class).annotatedWith(named("constructor")).toConstructor(cFoo).in(scope);
    }
  }

  private static interface Foo {}

  private static class FooImpl implements Foo {
    @Inject
    public FooImpl() {}

    private static Constructor<FooImpl> cxtor() {
      try {
        return FooImpl.class.getConstructor();
      } catch (SecurityException e) {
        throw new RuntimeException(e);
      } catch (NoSuchMethodException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static class FooProvider implements Provider<Foo> {
    @Override
    public Foo get() {
      return new FooImpl();
    }
  }

  private static class Bar implements Foo {
    @Inject
    public Bar() {}

    private static Constructor<Bar> cxtor() {
      try {
        return Bar.class.getConstructor();
      } catch (SecurityException e) {
        throw new RuntimeException(e);
      } catch (NoSuchMethodException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static class BarProvider implements Provider<Foo> {
    @Override
    public Foo get() {
      return new Bar();
    }
  }

  private static class HashEqualsTester implements Provider<Object> {
    private String equality;
    private boolean throwOnEquals;
    private boolean throwOnHashcode;

    @Override
    public boolean equals(Object obj) {
      if (throwOnEquals) {
        throw new RuntimeException();
      } else if (obj instanceof HashEqualsTester) {
        HashEqualsTester o = (HashEqualsTester) obj;
        if (o.throwOnEquals) {
          throw new RuntimeException();
        }
        if (equality == null && o.equality == null) {
          return this == o;
        } else {
          return Objects.equal(equality, o.equality);
        }
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      if (throwOnHashcode) {
        throw new RuntimeException();
      } else {
        return super.hashCode();
      }
    }

    @Override
    public Object get() {
      return new Object();
    }
  }
}
