/**
 * Copyright (C) 2009 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.googlecode.guice;

import static com.google.inject.Asserts.assertContains;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Scope;
import com.google.inject.Scopes;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.util.Providers;

import junit.framework.TestCase;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Qualifier;
import javax.inject.Singleton;

public class Jsr330Test extends TestCase {

  private final B b = new B();
  private final C c = new C();
  private final D d = new D();
  private final E e = new E();

  @Override protected void setUp() throws Exception {
    J.nextInstanceId = 0;
    K.nextInstanceId = 0;
  }

  public void testInject() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(B.class).toInstance(b);
        bind(C.class).toInstance(c);
        bind(D.class).toInstance(d);
        bind(E.class).toInstance(e);
        bind(A.class);
      }
    });

    A a = injector.getInstance(A.class);
    assertSame(b, a.b);
    assertSame(c, a.c);
    assertSame(d, a.d);
    assertSame(e, a.e);
  }

  public void testQualifiedInject() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(B.class).annotatedWith(Names.named("jodie")).toInstance(b);
        bind(C.class).annotatedWith(Red.class).toInstance(c);
        bind(D.class).annotatedWith(RED).toInstance(d);
        bind(E.class).annotatedWith(Names.named("jesse")).toInstance(e);
        bind(F.class);
      }
    });

    F f = injector.getInstance(F.class);
    assertSame(b, f.b);
    assertSame(c, f.c);
    assertSame(d, f.d);
    assertSame(e, f.e);
  }

  public void testProviderInject() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(B.class).annotatedWith(Names.named("jodie")).toInstance(b);
        bind(C.class).toInstance(c);
        bind(D.class).annotatedWith(RED).toInstance(d);
        bind(E.class).toInstance(e);
        bind(G.class);
      }
    });

    G g = injector.getInstance(G.class);
    assertSame(b, g.bProvider.get());
    assertSame(c, g.cProvider.get());
    assertSame(d, g.dProvider.get());
    assertSame(e, g.eProvider.get());
  }

  public void testScopeAnnotation() {
    final TestScope scope = new TestScope();

    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(B.class).in(scope);
        bind(C.class).in(TestScoped.class);
        bindScope(TestScoped.class, scope);
      }
    });

    B b = injector.getInstance(B.class);
    assertSame(b, injector.getInstance(B.class));
    assertSame(b, injector.getInstance(B.class));

    C c = injector.getInstance(C.class);
    assertSame(c, injector.getInstance(C.class));
    assertSame(c, injector.getInstance(C.class));

    H h = injector.getInstance(H.class);
    assertSame(h, injector.getInstance(H.class));
    assertSame(h, injector.getInstance(H.class));

    scope.reset();

    assertNotSame(b, injector.getInstance(B.class));
    assertNotSame(c, injector.getInstance(C.class));
    assertNotSame(h, injector.getInstance(H.class));
  }
  
  public void testSingleton() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(B.class).in(Singleton.class);
      }
    });

    B b = injector.getInstance(B.class);
    assertSame(b, injector.getInstance(B.class));
    assertSame(b, injector.getInstance(B.class));

    J j = injector.getInstance(J.class);
    assertSame(j, injector.getInstance(J.class));
    assertSame(j, injector.getInstance(J.class));
  }

  public void testEagerSingleton() {
    Guice.createInjector(Stage.PRODUCTION, new AbstractModule() {
      protected void configure() {
        bind(J.class);
        bind(K.class).in(Singleton.class);
      }
    });

    assertEquals(1, J.nextInstanceId);
    assertEquals(1, K.nextInstanceId);
  }
  
  public void testScopesIsSingleton() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(J.class);
        bind(K.class).in(Singleton.class);
      }
    });

    assertTrue(Scopes.isSingleton(injector.getBinding(J.class)));
    assertTrue(Scopes.isSingleton(injector.getBinding(K.class)));
  }

  public void testInjectingFinalFieldsIsForbidden() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(L.class);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) Injected field " + L.class.getName() + ".b cannot be final.");
    }
  }

  public void testInjectingAbstractMethodsIsForbidden() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(M.class);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) Injected method " + AbstractM.class.getName() + ".setB() cannot be abstract.");
    }
  }

  public void testInjectingMethodsWithTypeParametersIsForbidden() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(N.class);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), "1) Injected method " + N.class.getName()
          + ".setB() cannot declare type parameters of its own.");
    }
  }

  public void testInjectingMethodsWithNonVoidReturnTypes() {
    Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(P.class);
      }
    });
  }

  /**
   * This test verifies that we can compile bindings to provider instances
   * whose compile-time type implements javax.inject.Provider but not
   * com.google.inject.Provider. For binary compatibility, we don't (and won't)
   * support binding to instances of javax.inject.Provider.
   */
  public void testBindProviderClass() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(B.class).toProvider(BProvider.class);
        bind(B.class).annotatedWith(Names.named("1")).toProvider(BProvider.class);
        bind(B.class).annotatedWith(Names.named("2")).toProvider(Key.get(BProvider.class));
        bind(B.class).annotatedWith(Names.named("3")).toProvider(TypeLiteral.get(BProvider.class));
      }
    });
    
    injector.getInstance(Key.get(B.class));
    injector.getInstance(Key.get(B.class, Names.named("1")));
    injector.getInstance(Key.get(B.class, Names.named("2")));
    injector.getInstance(Key.get(B.class, Names.named("3")));
  }

  public void testGuicify330Provider() {
    Provider<String> jsr330Provider = new Provider<String>() {
      public String get() {
        return "A";
      }

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

    com.google.inject.Provider<String> guicified = Providers.guicify(jsr330Provider);
    assertEquals("guicified(jsr330Provider)", guicified.toString());
    assertEquals("A", guicified.get());

    // when you guicify the Guice-friendly, it's a no-op
    assertSame(guicified, Providers.guicify(guicified));
    
    assertFalse(guicified instanceof HasDependencies);
  }
  
  public void testGuicifyWithDependencies() {
    Provider<String> jsr330Provider = new Provider<String>() {
      @Inject double d;
      int i;
      @Inject void injectMe(int i) {
        this.i = i;
      }
      
      public String get() {
        return  d + "-" + i;
      }
    };
    
    final com.google.inject.Provider<String> guicified =
        Providers.guicify(jsr330Provider);
    assertTrue(guicified instanceof HasDependencies);
    Set<Dependency<?>> actual = ((HasDependencies)guicified).getDependencies();
    validateDependencies(actual, jsr330Provider.getClass());
    
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(String.class).toProvider(guicified);
        bind(int.class).toInstance(1);
        bind(double.class).toInstance(2.0d);
      }
    });
    
    Binding<String> binding = injector.getBinding(String.class);
    assertEquals("2.0-1", binding.getProvider().get());
    validateDependencies(actual, jsr330Provider.getClass());
  }
  
  private void validateDependencies(Set<Dependency<?>> actual, Class<?> owner) {
    assertEquals(actual.toString(), 2, actual.size());
    Dependency<?> dDep = null;
    Dependency<?> iDep = null;
    for(Dependency<?> dep : actual) {
      if(dep.getKey().equals(Key.get(Double.class))) {
        dDep = dep;
      } else if(dep.getKey().equals(Key.get(Integer.class))) {
        iDep = dep;
      }
    }
    assertNotNull(dDep);
    assertNotNull(iDep);
    assertEquals(TypeLiteral.get(owner), dDep.getInjectionPoint().getDeclaringType());
    assertEquals("d", dDep.getInjectionPoint().getMember().getName());
    assertEquals(-1, dDep.getParameterIndex());
    
    assertEquals(TypeLiteral.get(owner), iDep.getInjectionPoint().getDeclaringType());
    assertEquals("injectMe", iDep.getInjectionPoint().getMember().getName());
    assertEquals(0, iDep.getParameterIndex());
  }

  static class A {
    final B b;
    @Inject C c;
    D d;
    E e;

    @Inject A(B b) {
      this.b = b;
    }

    @Inject void injectD(D d, E e) {
      this.d = d;
      this.e = e;
    }
  }

  static class B {}
  static class C {}
  static class D {}
  static class E {}

  static class F {
    final B b;
    @Inject @Red C c;
    D d;
    E e;

    @Inject F(@Named("jodie") B b) {
      this.b = b;
    }

    @Inject void injectD(@Red D d, @Named("jesse") E e) {
      this.d = d;
      this.e = e;
    }
  }

  @Qualifier @Retention(RUNTIME)
  @interface Red {}

  public static final Red RED = new Red() {
    public Class<? extends Annotation> annotationType() {
      return Red.class;
    }

    @Override public boolean equals(Object obj) {
      return obj instanceof Red;
    }

    @Override public int hashCode() {
      return 0;
    }
  };

  static class G {
    final Provider<B> bProvider;
    @Inject Provider<C> cProvider;
    Provider<D> dProvider;
    Provider<E> eProvider;

    @Inject G(@Named("jodie") Provider<B> bProvider) {
      this.bProvider = bProvider;
    }

    @Inject void injectD(@Red Provider<D> dProvider, Provider<E> eProvider) {
      this.dProvider = dProvider;
      this.eProvider = eProvider;
    }
  }

  @javax.inject.Scope @Retention(RUNTIME)
  @interface TestScoped {}

  static class TestScope implements Scope {
    private int now = 0;

    public <T> com.google.inject.Provider<T> scope(Key<T> key,
        final com.google.inject.Provider<T> unscoped) {
      return new com.google.inject.Provider<T>() {
        private T value;
        private int snapshotTime = -1;

        public T get() {
          if (snapshotTime != now) {
            value = unscoped.get();
            snapshotTime = now;
          }
          return value;
        }
      };
    }

    public void reset() {
      now++;
    }
  }

  @TestScoped
  static class H {}

  @Singleton
  static class J {
    static int nextInstanceId = 0;
    int instanceId = nextInstanceId++;
  }

  static class K {
    static int nextInstanceId = 0;
    int instanceId = nextInstanceId++;
  }

  static class L {
    @SuppressWarnings("InjectJavaxInjectOnFinalField")
    @Inject
    final B b = null;
  }

  static abstract class AbstractM {
    @SuppressWarnings("InjectJavaxInjectOnAbstractMethod")
    @Inject
    abstract void setB(B b);
  }

  static class M extends AbstractM {
    void setB(B b) {}
  }

  static class N {
    @Inject <T> void setB(B b) {}
  }

  static class P {
    @Inject B setB(B b) {
      return b;
    }
  }

  static class BProvider implements Provider<B> {
    public B get() {
      return new B();
    }
  }
}
