/**
 * Copyright (C) 2006 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.assertContains;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;

import junit.framework.TestCase;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author crazybob@google.com (Bob Lee)
 * @author sameb@google.com (Sam Berlin)
 */
public class CircularDependencyTest extends TestCase {
  
  @Override
  protected void setUp() throws Exception {
    AImpl.nextId = 0;
    BImpl.nextId = 0;
  }

  public void testCircularlyDependentConstructors()
      throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(A.class).to(AImpl.class);
        bind(B.class).to(BImpl.class);
      }
    });
    assertCircularDependencies(injector);
  }
  
  public void testCircularlyDependentConstructorsWithProviderMethods()
      throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {}
      
      @Provides @Singleton A a(B b) { return new AImpl(b); }
      @Provides B b(A a) { return new BImpl(a); }
    });
    assertCircularDependencies(injector);
  }
  
  public void testCircularlyDependentConstructorsWithProviderInstances()
      throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(A.class).toProvider(new Provider<A>() {
          @Inject Provider<B> bp;
          public A get() {
            return new AImpl(bp.get());
          }
        }).in(Singleton.class);
        bind(B.class).toProvider(new Provider<B>() {
          @Inject Provider<A> ap;
          public B get() {
            return new BImpl(ap.get());
          }
        });
      }
    });
    assertCircularDependencies(injector);
  }
  
  public void testCircularlyDependentConstructorsWithProviderKeys()
      throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(A.class).toProvider(AP.class).in(Singleton.class);
        bind(B.class).toProvider(BP.class);
      }
    });
    assertCircularDependencies(injector);
  }
  
  public void testCircularlyDependentConstructorsWithProvidedBy()
      throws CreationException {
    Injector injector = Guice.createInjector();
    assertCircularDependencies(injector);
  }
  
  private void assertCircularDependencies(Injector injector) {
    A a = injector.getInstance(A.class);
    assertNotNull(a.getB().getA());
    assertEquals(0, a.id());
    assertEquals(a.id(), a.getB().getA().id());
    assertEquals(0, a.getB().id());
    assertEquals(1, AImpl.nextId);
    assertEquals(1, BImpl.nextId);
    assertSame(a, injector.getInstance(A.class));
  }

  @ProvidedBy(AutoAP.class)
  public interface A {
    B getB();
    int id();
  }

  @Singleton
  static class AImpl implements A {
    static int nextId;
    int id = nextId++;
    
    final B b;
    @Inject public AImpl(B b) {
      this.b = b;
    }
    public int id() {
      return id;
    }
    public B getB() {
      return b;
    }
  }
  
  static class AP implements Provider<A> {
    @Inject Provider<B> bp;
    public A get() {
      return new AImpl(bp.get());
    }
  }
  
  @Singleton
  static class AutoAP implements Provider<A> {
    @Inject Provider<B> bp;
    A a;
    
    public A get() {
      if (a == null) {
        a = new AImpl(bp.get());
      }
      return a;
    }
  }

  @ProvidedBy(BP.class)
  public interface B {
    A getA();
    int id();
  }

  static class BImpl implements B {
    static int nextId;
    int id = nextId++;
    
    final A a;
    @Inject public BImpl(A a) {
      this.a = a;
    }
    public int id() {
      return id;
    }
    public A getA() {
      return a;
    }
  }
  
  static class BP implements Provider<B> {
    Provider<A> ap;
    @Inject BP(Provider<A> ap) {
      this.ap = ap;
    }
    public B get() {
      return new BImpl(ap.get());
    }
  }

  public void testUnresolvableCircularDependency() {
    try {
      Guice.createInjector().getInstance(C.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C.class.getName() + " to support a circular dependency, ",
          "but it is not an interface.");
    }
  }
  
  public void testUnresolvableCircularDependenciesWithProviderInstances() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override protected void configure() {}        
        @Provides C c(D d) { return null; }
        @Provides D d(C c) { return null; }
      }).getInstance(C.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C.class.getName() + " to support a circular dependency, ",
          "but it is not an interface.");
    }
  }
  
  public void testUnresolvableCircularDependenciesWithProviderKeys() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override protected void configure() {
          bind(C2.class).toProvider(C2P.class);
          bind(D2.class).toProvider(D2P.class);
        }
      }).getInstance(C2.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C2.class.getName() + " to support a circular dependency, ",
          "but it is not an interface.");
    }
  }
  
  public void testUnresolvableCircularDependenciesWithProvidedBy() {
    try {
      Guice.createInjector().getInstance(C2.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C2.class.getName() + " to support a circular dependency, ",
          "but it is not an interface.");
    }
  }

  static class C {
    @Inject C(D d) {}
  }
  static class D {
    @Inject D(C c) {}
  }
  
  static class C2P implements Provider<C2> {
    @Inject Provider<D2> dp;
    public C2 get() {
      dp.get();
      return null;
    }
  }
  static class D2P implements Provider<D2> {
    @Inject Provider<C2> cp;
    public D2 get() {
      cp.get();
      return null;
    }
  }
  @ProvidedBy(C2P.class)
  static class C2 {
    @Inject C2(D2 d) {}
  }
  @ProvidedBy(D2P.class)
  static class D2 {
    @Inject D2(C2 c) {}
  }
  
  public void testDisabledCircularDependency() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          binder().disableCircularProxies();
        }
      }).getInstance(C.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C.class.getName() + " to support a circular dependency, ",
          "but circular proxies are disabled.");
    }
  }
  
  public void testDisabledCircularDependenciesWithProviderInstances() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override protected void configure() {
          binder().disableCircularProxies();
        }        
        @Provides C c(D d) { return null; }
        @Provides D d(C c) { return null; }
      }).getInstance(C.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C.class.getName() + " to support a circular dependency, ",
          "but circular proxies are disabled.");
    }
  }
  
  public void testDisabledCircularDependenciesWithProviderKeys() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override protected void configure() {
          binder().disableCircularProxies();
          bind(C2.class).toProvider(C2P.class);
          bind(D2.class).toProvider(D2P.class);
        }
      }).getInstance(C2.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C2.class.getName() + " to support a circular dependency, ",
          "but circular proxies are disabled.");
    }
  }
  
  public void testDisabledCircularDependenciesWithProvidedBy() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          binder().disableCircularProxies();
        }
      }).getInstance(C2.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + C2.class.getName() + " to support a circular dependency, ",
          "but circular proxies are disabled.");
    }
  }

  /**
   * As reported by issue 349, we give a lousy trace when a class is circularly
   * dependent on itself in multiple ways.
   */
  public void testCircularlyDependentMultipleWays() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        binder.bind(A.class).to(E.class);
        binder.bind(B.class).to(E.class);
      }
    });
    injector.getInstance(A.class);
  }
  
  public void testDisablingCircularProxies() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        binder().disableCircularProxies();
        binder.bind(A.class).to(E.class);
        binder.bind(B.class).to(E.class);
      }
    });
    
    try {
      injector.getInstance(A.class);
      fail("expected exception");
    } catch(ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + A.class.getName() + " to support a circular dependency, but circular proxies are disabled", 
          "Tried proxying " + B.class.getName() + " to support a circular dependency, but circular proxies are disabled");
    }
  }

  @Singleton
  static class E implements A, B {
    @Inject
    public E(A a, B b) {}

    public B getB() {
      return this;
    }

    public A getA() {
      return this;
    }
    
    public int id() {
      return 0;
    }
  }


  public void testCircularDependencyProxyDelegateNeverInitialized() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(F.class).to(RealF.class);
        bind(G.class).to(RealG.class);
      }
    });
    F f = injector.getInstance(F.class);
    assertEquals("F", f.g().f().toString());
    assertEquals("G", f.g().f().g().toString());

  }

  public interface F {
    G g();
  }

  @Singleton
  public static class RealF implements F {
    private final G g;
    @Inject RealF(G g) {
      this.g = g;
    }

    public G g() {
      return g;
    }

    @Override public String toString() {
      return "F";
    }
  }

  public interface G {
    F f();
  }

  @Singleton
  public static class RealG implements G {
    private final F f;
    @Inject RealG(F f) {
      this.f = f;
    }

    public F f() {
      return f;
    }

    @Override public String toString() {
      return "G";
    }
  }
  
  /**
   * Tests that ProviderInternalFactory can detect circular dependencies
   * before it gets to Scopes.SINGLETON.  This is especially important
   * because the failure in Scopes.SINGLETON doesn't have enough context to
   * provide a decent error message.
   */
  public void testCircularDependenciesDetectedEarlyWhenDependenciesHaveDifferentTypes() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(Number.class).to(Integer.class);
      }
      
      @Provides @Singleton Integer provideInteger(List list) { 
        return new Integer(2);
      }
      
      @Provides List provideList(Integer integer) {
        return new ArrayList();
      }
    });
    try {
      injector.getInstance(Number.class);
      fail();
    } catch(ProvisionException expected) {
      assertContains(expected.getMessage(),
          "Tried proxying " + Integer.class.getName() + " to support a circular dependency, ",
          "but it is not an interface.");      
    }
  }
  
  public void testPrivateModulesDontTriggerCircularErrorsInProviders() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        install(new PrivateModule() {
          @Override
          protected void configure() {
            bind(Foo.class);
            expose(Foo.class);
          }
          @Provides String provideString(Bar bar) {
            return new String("private 1, " + bar.string);
          }
        });
        install(new PrivateModule() {
          @Override
          protected void configure() {
            bind(Bar.class);
            expose(Bar.class);
          }
          @Provides String provideString() {
            return new String("private 2");
          }
        });
      }
    });
    Foo foo = injector.getInstance(Foo.class);
    assertEquals("private 1, private 2", foo.string);
  }
  static class Foo {
    @Inject String string;
  }
  static class Bar {
    @Inject String string;
  }
  
  /**
   * When Scope Providers call their unscoped Provider's get() methods are
   * called, it's possible that the result is a circular proxy designed for one
   * specific parameter (not for all possible parameters). But custom scopes
   * typically cache the results without checking to see if the result is a
   * proxy. This leads to caching a result that is unsuitable for reuse for
   * other parameters.
   * 
   * This means that custom proxies have to do an
   *   {@code if(Scopes.isCircularProxy(..))}
   * in order to avoid exceptions.
   */
  public void testCustomScopeCircularProxies() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindScope(SimpleSingleton.class, new BasicSingleton());
        bind(H.class).to(HImpl.class);
        bind(I.class).to(IImpl.class);
        bind(J.class).to(JImpl.class);
      }
    });
    
    // The reason this happens is because the Scope gets these requests, in order:
    // entry: Key<IImpl> (1 - from getInstance call)
    // entry: Key<HImpl>
    // entry: Key<IImpl> (2 - circular dependency from HImpl)
    // result of 2nd Key<IImpl> - a com.google.inject.$Proxy, because it's a circular proxy
    // result of Key<HImpl> - an HImpl
    // entry: Key<JImpl>
    // entry: Key<IImpl> (3 - another circular dependency, this time from JImpl)    
    // At this point, if the first Key<Impl> result was cached, our cache would have
    //  Key<IImpl> caching to an instanceof of I, but not an an instanceof of IImpl.
    // If returned this, it would result in cglib giving a ClassCastException or
    // java reflection giving an IllegalArgumentException when filling in parameters
    // for the constructor, because JImpl wants an IImpl, not an I.
    
    try {
      injector.getInstance(IImpl.class);
      fail();
    } catch(ProvisionException pe) {
      assertContains(Iterables.getOnlyElement(pe.getErrorMessages()).getMessage(),
          "Tried proxying " + IImpl.class.getName()
          + " to support a circular dependency, but it is not an interface.");
    }
  }
  
  interface H {}
  interface I {}
  interface J {}
  @SimpleSingleton
  static class HImpl implements H {
     @Inject HImpl(I i) {}     
  }
  @SimpleSingleton
  static class IImpl implements I {
     @Inject IImpl(HImpl i, J j) {}
  }
  @SimpleSingleton
  static class JImpl implements J {
     @Inject JImpl(IImpl i) {}
  }
  
  @Target({ ElementType.TYPE, ElementType.METHOD })
  @Retention(RUNTIME)
  @ScopeAnnotation
  public @interface SimpleSingleton {}
  public static class BasicSingleton implements Scope {
    private static Map<Key, Object> cache = Maps.newHashMap();
    public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
      return new Provider<T>() {
        @SuppressWarnings("unchecked")
        public T get() {
          if (!cache.containsKey(key)) {
            T t = unscoped.get();
            if (Scopes.isCircularProxy(t)) {
              return t;
            }
            cache.put(key, t);
          }
          return (T)cache.get(key);
        }
      };
    }
  }
}
