/**
 * 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 junit.framework.TestCase;

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;

/**
 * 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!");
        }
      });
    }
  }
  
  private static abstract 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);
    }
    
    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 {
    @Override protected void configure() {}

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

  private static class SimpleProviderModule extends AbstractModule {
    @Override protected void configure() {}

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

    @Override
    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);
    }
    
    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;
    }
    
    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;
    }
    
    
    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> {
    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> {
    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();
      }
    }
    
    public Object get() {
      return new Object();
    }
  }
  
}
