/**
 * 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 com.google.inject.Asserts.assertNotSerializable;
import static java.lang.annotation.RetentionPolicy.RUNTIME;


import junit.framework.TestCase;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author crazybob@google.com (Bob Lee)
 */

public class InjectorTest extends TestCase {

  @Retention(RUNTIME)
  @BindingAnnotation @interface Other {}

  @Retention(RUNTIME)
  @BindingAnnotation @interface S {}

  @Retention(RUNTIME)
  @BindingAnnotation @interface I {}

  public void testToStringDoesNotInfinitelyRecurse() {
    Injector injector = Guice.createInjector(Stage.TOOL);
    injector.toString();
    injector.getBinding(Injector.class).toString();
  }

  public void testProviderMethods() throws CreationException {
    final SampleSingleton singleton = new SampleSingleton();
    final SampleSingleton other = new SampleSingleton();

    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(SampleSingleton.class).toInstance(singleton);
        bind(SampleSingleton.class)
            .annotatedWith(Other.class)
            .toInstance(other);
      }
    });

    assertSame(singleton,
        injector.getInstance(Key.get(SampleSingleton.class)));
    assertSame(singleton, injector.getInstance(SampleSingleton.class));

    assertSame(other,
        injector.getInstance(Key.get(SampleSingleton.class, Other.class)));
  }

  static class SampleSingleton {}

  public void testInjection() throws CreationException {
    Injector injector = createFooInjector();
    Foo foo = injector.getInstance(Foo.class);

    assertEquals("test", foo.s);
    assertEquals("test", foo.bar.getTee().getS());
    assertSame(foo.bar, foo.copy);
    assertEquals(5, foo.i);
    assertEquals(5, foo.bar.getI());

    // Test circular dependency.
    assertSame(foo.bar, foo.bar.getTee().getBar());
  }

  private Injector createFooInjector() throws CreationException {
    return Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(Bar.class).to(BarImpl.class);
        bind(Tee.class).to(TeeImpl.class);
        bindConstant().annotatedWith(S.class).to("test");
        bindConstant().annotatedWith(I.class).to(5);
      }
    });
  }

  public void testGetInstance() throws CreationException {
    Injector injector = createFooInjector();

    Bar bar = injector.getInstance(Key.get(Bar.class));
    assertEquals("test", bar.getTee().getS());
    assertEquals(5, bar.getI());
  }

  public void testIntAndIntegerAreInterchangeable()
      throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bindConstant().annotatedWith(I.class).to(5);
      }
    });

    IntegerWrapper iw = injector.getInstance(IntegerWrapper.class);
    assertEquals(5, (int) iw.i);
  }

  public void testInjectorApiIsNotSerializable() throws IOException {
    Injector injector = Guice.createInjector();
    assertNotSerializable(injector);
    assertNotSerializable(injector.getProvider(String.class));
    assertNotSerializable(injector.getBinding(String.class));
    for (Binding<?> binding : injector.getBindings().values()) {
      assertNotSerializable(binding);
    }
  }

  static class IntegerWrapper {
    @Inject @I Integer i;
  }

  static class Foo {

    @Inject Bar bar;
    @Inject Bar copy;

    @Inject @S String s;

    int i;

    @Inject
    void setI(@I int i) {
      this.i = i;
    }
  }

  interface Bar {

    Tee getTee();
    int getI();
  }

  @Singleton
  static class BarImpl implements Bar {

    @Inject @I int i;

    Tee tee;

    @Inject
    void initialize(Tee tee) {
      this.tee = tee;
    }

    public Tee getTee() {
      return tee;
    }

    public int getI() {
      return i;
    }
  }

  interface Tee {

    String getS();
    Bar getBar();
  }

  static class TeeImpl implements Tee {

    final String s;
    @Inject Bar bar;

    @Inject
    TeeImpl(@S String s) {
      this.s = s;
    }

    public String getS() {
      return s;
    }

    public Bar getBar() {
      return bar;
    }
  }

  public void testInjectStatics() throws CreationException {
    Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bindConstant().annotatedWith(S.class).to("test");
        bindConstant().annotatedWith(I.class).to(5);
        requestStaticInjection(Static.class);
      }
    });

    assertEquals("test", Static.s);
    assertEquals(5, Static.i);
  }

  public void testInjectStaticInterface() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          requestStaticInjection(Interface.class);
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      Asserts.assertContains(
          ce.getMessage(),
          "1) " + Interface.class.getName()
              + " is an interface, but interfaces have no static injection points.",
          "at " + InjectorTest.class.getName(),
          "configure");
    }
  }

  private static interface Interface {}

  static class Static {

    @Inject @I static int i;

    static String s;

    @Inject static void setS(@S String s) {
      Static.s = s;
    }
  }

  public void testPrivateInjection() throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(String.class).toInstance("foo");
        bind(int.class).toInstance(5);
      }
    });

    Private p = injector.getInstance(Private.class);
    assertEquals("foo", p.fromConstructor);
    assertEquals(5, p.fromMethod);
  }

  static class Private {
    String fromConstructor;
    int fromMethod;

    @Inject
    private Private(String fromConstructor) {
      this.fromConstructor = fromConstructor;
    }

    @Inject
    private void setInt(int i) {
      this.fromMethod = i;
    }
  }

  public void testProtectedInjection() throws CreationException {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(String.class).toInstance("foo");
        bind(int.class).toInstance(5);
      }
    });

    Protected p = injector.getInstance(Protected.class);
    assertEquals("foo", p.fromConstructor);
    assertEquals(5, p.fromMethod);
  }

  static class Protected {
    String fromConstructor;
    int fromMethod;

    @Inject
    protected Protected(String fromConstructor) {
      this.fromConstructor = fromConstructor;
    }

    @Inject
    protected void setInt(int i) {
      this.fromMethod = i;
    }
  }

  public void testInstanceInjectionHappensAfterFactoriesAreSetUp() {
    Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(Object.class).toInstance(new Object() {
          @Inject Runnable r;
        });

        bind(Runnable.class).to(MyRunnable.class);
      }
    });
  }

  public void testSubtypeNotProvided() {
    try {
      Guice.createInjector().getInstance(Money.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          Tree.class.getName() + " doesn't provide instances of " + Money.class.getName(),
          "while locating ", Tree.class.getName(),
          "while locating ", Money.class.getName());
    }
  }

  public void testNotASubtype() {
    try {
      Guice.createInjector().getInstance(PineTree.class);
      fail();
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(),
          Tree.class.getName() + " doesn't extend " + PineTree.class.getName(),
          "while locating ", PineTree.class.getName());
    }
  }

  public void testRecursiveImplementationType() {
    try {
      Guice.createInjector().getInstance(SeaHorse.class);
      fail();
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(),
          "@ImplementedBy points to the same class it annotates.",
          "while locating ", SeaHorse.class.getName());
    }
  }

  public void testRecursiveProviderType() {
    try {
      Guice.createInjector().getInstance(Chicken.class);
      fail();
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(),
          "@ProvidedBy points to the same class it annotates",
          "while locating ", Chicken.class.getName());
    }
  }

  static class MyRunnable implements Runnable {
   public void run() {}
  }

  @ProvidedBy(Tree.class)
  static class Money {}

  static class Tree implements Provider<Object> {
    public Object get() {
      return "Money doesn't grow on trees";
    }
  }

  @ImplementedBy(Tree.class)
  static class PineTree extends Tree {}

  @ImplementedBy(SeaHorse.class)
  static class SeaHorse {}

  @ProvidedBy(Chicken.class)
  static class Chicken implements Provider<Chicken> {
    public Chicken get() {
      return this;
    }
  }

  public void testJitBindingFromAnotherThreadDuringInjection() {
    final ExecutorService executorService = Executors.newSingleThreadExecutor();
    final AtomicReference<JustInTime> got = new AtomicReference<JustInTime>();

    Guice.createInjector(new AbstractModule() {
      protected void configure() {
        requestInjection(new Object() {
          @Inject void initialize(final Injector injector)
              throws ExecutionException, InterruptedException {
            Future<JustInTime> future = executorService.submit(new Callable<JustInTime>() {
              public JustInTime call() throws Exception {
                return injector.getInstance(JustInTime.class);
              }
            });
            got.set(future.get());
          }
        });
      }
    });

    assertNotNull(got.get());
  }

  static class JustInTime {}
}
