/**
 * Copyright (C) 2007 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.asModuleChain;
import static com.google.inject.Asserts.assertContains;
import static com.google.inject.Asserts.assertNotSerializable;
import static com.google.inject.Asserts.getDeclaringSourcePart;
import static com.google.inject.Asserts.isIncludeStackTraceOff;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.google.inject.spi.Message;
import com.google.inject.util.Providers;

import junit.framework.TestCase;

import java.io.IOException;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

/**
 * @author crazybob@google.com (Bob Lee)
 */
public class BinderTest extends TestCase {

  private final Logger loggerToWatch = Logger.getLogger(Guice.class.getName());

  private final List<LogRecord> logRecords = Lists.newArrayList();
  private final Handler fakeHandler = new Handler() {
    @Override
    public void publish(LogRecord logRecord) {
      logRecords.add(logRecord);
    }
    @Override
    public void flush() {}
    @Override
    public void close() throws SecurityException {}
  };

  Provider<Foo> fooProvider;

  @Override protected void setUp() throws Exception {
    super.setUp();
    loggerToWatch.addHandler(fakeHandler);
  }

  @Override protected void tearDown() throws Exception {
    loggerToWatch.removeHandler(fakeHandler);
    super.tearDown();
  }

  public void testProviderFromBinder() {
    Guice.createInjector(new Module() {
      public void configure(Binder binder) {
        fooProvider = binder.getProvider(Foo.class);

        try {
          fooProvider.get();
        } catch (IllegalStateException e) { /* expected */ }
      }
    });

    assertNotNull(fooProvider.get());
  }

  static class Foo {}

  public void testMissingBindings() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        public void configure() {
          getProvider(Runnable.class);
          bind(Comparator.class);
          requireBinding(Key.get(new TypeLiteral<Callable<String>>() {}));
          bind(Date.class).annotatedWith(Names.named("date"));
        }
      });
    } catch (CreationException e) {
      assertEquals(4, e.getErrorMessages().size());
      String segment1 = "No implementation for " + Comparator.class.getName() + " was bound.";
      String segment2 = "No implementation for java.util.Date annotated with @"
          + Named.class.getName() + "(value=date) was bound.";
      String segment3 = "No implementation for java.lang.Runnable was bound.";
      String segment4 = " No implementation for java.util.concurrent.Callable<java.lang.String> was"
          + " bound.";
      String atSegment = "at " + getClass().getName();
      String sourceFileName = getDeclaringSourcePart(getClass());
      if (isIncludeStackTraceOff()) {
        assertContains(e.getMessage(),
            segment1, atSegment, sourceFileName,
            segment2, atSegment, sourceFileName,
            segment3, atSegment, sourceFileName,
            segment4, atSegment, sourceFileName);
      } else {
        assertContains(e.getMessage(),
            segment3, atSegment, sourceFileName,
            segment1, atSegment, sourceFileName,
            segment4, atSegment, sourceFileName,
            segment2, atSegment, sourceFileName);
      }
    }
  }

  public void testMissingDependency() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        public void configure() {
          bind(NeedsRunnable.class);
        }
      });
    } catch (CreationException e) {
      assertEquals(1, e.getErrorMessages().size());
      assertContains(e.getMessage(),
          "No implementation for java.lang.Runnable was bound.",
          "for field at " + NeedsRunnable.class.getName(), ".runnable(BinderTest.java:",
          "at " + getClass().getName(), getDeclaringSourcePart(getClass()));
    }
  }

  static class NeedsRunnable {
    @Inject Runnable runnable;
  }

  public void testDanglingConstantBinding() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override public void configure() {
          bindConstant();
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) Missing constant value. Please call to(...).",
          "at " + getClass().getName());
    }
  }

  public void testRecursiveBinding() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override public void configure() {
          bind(Runnable.class).to(Runnable.class);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) Binding points to itself.",
          "at " + getClass().getName(), getDeclaringSourcePart(getClass()));
    }
  }

  public void testBindingNullConstant() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override public void configure() {
          String none = null;
          bindConstant().annotatedWith(Names.named("nullOne")).to(none);
          bind(String.class).annotatedWith(Names.named("nullTwo")).toInstance(none);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) Binding to null instances is not allowed. Use toProvider(Providers.of(null))",
          "2) Binding to null instances is not allowed. Use toProvider(Providers.of(null))");
    }
  }

  public void testToStringOnBinderApi() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override public void configure() {
          assertEquals("Binder", binder().toString());
          assertEquals("Provider<java.lang.Integer>", getProvider(Integer.class).toString());
          assertEquals("Provider<java.util.List<java.lang.String>>",
              getProvider(Key.get(new TypeLiteral<List<String>>() {})).toString());

          assertEquals("BindingBuilder<java.lang.Integer>",
              bind(Integer.class).toString());
          assertEquals("BindingBuilder<java.lang.Integer>",
              bind(Integer.class).annotatedWith(Names.named("a")).toString());
          assertEquals("ConstantBindingBuilder", bindConstant().toString());
          assertEquals("ConstantBindingBuilder",
              bindConstant().annotatedWith(Names.named("b")).toString());
          assertEquals("AnnotatedElementBuilder",
              binder().newPrivateBinder().expose(Integer.class).toString());
        }
      });
      fail();
    } catch (CreationException ignored) {
    }
  }

  public void testNothingIsSerializableInBinderApi() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override public void configure() {
          try {
            assertNotSerializable(binder());
            assertNotSerializable(getProvider(Integer.class));
            assertNotSerializable(getProvider(Key.get(new TypeLiteral<List<String>>() {})));
            assertNotSerializable(bind(Integer.class));
            assertNotSerializable(bind(Integer.class).annotatedWith(Names.named("a")));
            assertNotSerializable(bindConstant());
            assertNotSerializable(bindConstant().annotatedWith(Names.named("b")));
          } catch (IOException e) {
            fail(e.getMessage());
          }
        }
      });
      fail();
    } catch (CreationException ignored) {
    }
  }

  /**
   * Although {@code String[].class} isn't equal to {@code new
   * GenericArrayTypeImpl(String.class)}, Guice should treat these two types
   * interchangeably.
   */
  public void testArrayTypeCanonicalization() {
    final String[] strings = new String[] { "A" };
    final Integer[] integers = new Integer[] { 1 };

    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(String[].class).toInstance(strings);
        bind(new TypeLiteral<Integer[]>() {}).toInstance(integers);
      }
    });

    assertSame(integers, injector.getInstance(Key.get(new TypeLiteral<Integer[]>() {})));
    assertSame(integers, injector.getInstance(new Key<Integer[]>() {}));
    assertSame(integers, injector.getInstance(Integer[].class));
    assertSame(strings, injector.getInstance(Key.get(new TypeLiteral<String[]>() {})));
    assertSame(strings, injector.getInstance(new Key<String[]>() {}));
    assertSame(strings, injector.getInstance(String[].class));

    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          bind(String[].class).toInstance(new String[] { "A" });
          bind(new TypeLiteral<String[]>() {}).toInstance(new String[] { "B" });
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) A binding to java.lang.String[] was already configured at " + getClass().getName(),
          "at " + getClass().getName(), getDeclaringSourcePart(getClass()));
      assertContains(expected.getMessage(), "1 error");
    }

    // passes because duplicates are ignored
    injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(String[].class).toInstance(strings);
        bind(new TypeLiteral<String[]>() {}).toInstance(strings);
      }
    });
    assertSame(strings, injector.getInstance(Key.get(new TypeLiteral<String[]>() {})));
    assertSame(strings, injector.getInstance(new Key<String[]>() {}));
    assertSame(strings, injector.getInstance(String[].class));
  }

  static class ParentModule extends AbstractModule {
    @Override protected void configure() {
      install(new FooModule());
      install(new BarModule());
    }
  }
  static class FooModule extends AbstractModule {
    @Override protected void configure() {
      install(new ConstantModule("foo"));
    }
  }
  static class BarModule extends AbstractModule {
    @Override protected void configure() {
      install(new ConstantModule("bar"));
    }
  }
  static class ConstantModule extends AbstractModule {
    private final String constant;
    ConstantModule(String constant) {
      this.constant = constant;
    }
    @Override protected void configure() {
      bind(String.class).toInstance(constant);
    }
  }

  /**
   * Binding something to two different things should give an error.
   */
  public void testSettingBindingTwice() {
    try {
      Guice.createInjector(new ParentModule());
      fail();
    } catch(CreationException expected) {
      assertContains(expected.getMessage(),
        "1) A binding to java.lang.String was already configured at " + ConstantModule.class.getName(),
        asModuleChain(ParentModule.class, FooModule.class, ConstantModule.class),
        "at " + ConstantModule.class.getName(), getDeclaringSourcePart(getClass()),
        asModuleChain(ParentModule.class, BarModule.class, ConstantModule.class));
      assertContains(expected.getMessage(), "1 error");
    }
  }

  /**
   * Binding an @ImplementedBy thing to something else should also fail.
   */
  public void testSettingAtImplementedByTwice() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          bind(HasImplementedBy1.class);
          bind(HasImplementedBy1.class).toInstance(new HasImplementedBy1() {});
        }
      });
      fail();
    } catch(CreationException expected) {
      expected.printStackTrace();
      assertContains(expected.getMessage(),
        "1) A binding to " + HasImplementedBy1.class.getName()
        + " was already configured at " + getClass().getName(),
        "at " + getClass().getName(), getDeclaringSourcePart(getClass()));
      assertContains(expected.getMessage(), "1 error");
    }
  }

  /**
   * See issue 614, Problem One
   * https://github.com/google/guice/issues/614
   */
  public void testJitDependencyDoesntBlockOtherExplicitBindings() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(HasImplementedByThatNeedsAnotherImplementedBy.class);
        bind(HasImplementedBy1.class).toInstance(new HasImplementedBy1() {});
      }
    });
    injector.getAllBindings(); // just validate it doesn't throw.
    // Also validate that we're using the explicit (and not @ImplementedBy) implementation
    assertFalse(injector.getInstance(HasImplementedBy1.class) instanceof ImplementsHasImplementedBy1);
  }

  /**
   * See issue 614, Problem Two
   * https://github.com/google/guice/issues/id=614
   */
  public void testJitDependencyCanUseExplicitDependencies() {
    Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(HasImplementedByThatWantsExplicit.class);
        bind(JustAnInterface.class).toInstance(new JustAnInterface() {});
      }
    });
  }

  /**
   * Untargetted bindings should follow @ImplementedBy and @ProvidedBy
   * annotations if they exist. Otherwise the class should be constructed
   * directly.
   */
  public void testUntargettedBinding() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(HasProvidedBy1.class);
        bind(HasImplementedBy1.class);
        bind(HasProvidedBy2.class);
        bind(HasImplementedBy2.class);
        bind(JustAClass.class);
      }
    });

    assertNotNull(injector.getInstance(HasProvidedBy1.class));
    assertNotNull(injector.getInstance(HasImplementedBy1.class));
    assertNotSame(HasProvidedBy2.class,
        injector.getInstance(HasProvidedBy2.class).getClass());
    assertSame(ExtendsHasImplementedBy2.class,
        injector.getInstance(HasImplementedBy2.class).getClass());
    assertSame(JustAClass.class, injector.getInstance(JustAClass.class).getClass());
  }

  public void testPartialInjectorGetInstance() {
    Injector injector = Guice.createInjector();
    try {
      injector.getInstance(MissingParameter.class);
      fail();
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(),
          "1) Could not find a suitable constructor in " + NoInjectConstructor.class.getName(),
          "at " + MissingParameter.class.getName() + ".<init>(BinderTest.java:");
    }
  }

  public void testUserReportedError() {
    final Message message = new Message(getClass(), "Whoops!");
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          addError(message);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertSame(message, Iterables.getOnlyElement(expected.getErrorMessages()));
    }
  }

  public void testUserReportedErrorsAreAlsoLogged() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          addError(new Message("Whoops!", new IllegalArgumentException()));
        }
      });
      fail();
    } catch (CreationException expected) {
    }

    LogRecord logRecord = Iterables.getOnlyElement(this.logRecords);
    assertContains(logRecord.getMessage(),
        "An exception was caught and reported. Message: java.lang.IllegalArgumentException");
  }

  public void testBindingToProvider() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          bind(new TypeLiteral<Provider<String>>() {}).toInstance(Providers.of("A"));
        }
      });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "1) Binding to Provider is not allowed.",
          "at " + BinderTest.class.getName(), getDeclaringSourcePart(getClass()));
    }
  }

  static class OuterCoreModule extends AbstractModule {
    @Override protected void configure() {
      install(new InnerCoreModule());
    }
  }
  static class InnerCoreModule extends AbstractModule {
    final Named red = Names.named("red");

    @Override protected void configure() {
      bind(AbstractModule.class).annotatedWith(red)
      .toProvider(Providers.<AbstractModule>of(null));
      bind(Binder.class).annotatedWith(red).toProvider(Providers.<Binder>of(null));
      bind(Binding.class).annotatedWith(red).toProvider(Providers.<Binding>of(null));
      bind(Injector.class).annotatedWith(red).toProvider(Providers.<Injector>of(null));
      bind(Key.class).annotatedWith(red).toProvider(Providers.<Key>of(null));
      bind(Module.class).annotatedWith(red).toProvider(Providers.<Module>of(null));
      bind(Provider.class).annotatedWith(red).toProvider(Providers.<Provider>of(null));
      bind(Scope.class).annotatedWith(red).toProvider(Providers.<Scope>of(null));
      bind(Stage.class).annotatedWith(red).toProvider(Providers.<Stage>of(null));
      bind(TypeLiteral.class).annotatedWith(red).toProvider(Providers.<TypeLiteral>of(null));
      bind(new TypeLiteral<Key<String>>() {}).toProvider(Providers.<Key<String>>of(null));
    }
  }
  public void testCannotBindToGuiceTypes() {
    try {
      Guice.createInjector(new OuterCoreModule());
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "Binding to core guice framework type is not allowed: AbstractModule.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Binder.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Binding.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Injector.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Key.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Module.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to Provider is not allowed.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Scope.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Stage.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: TypeLiteral.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class),

          "Binding to core guice framework type is not allowed: Key.",
          "at " + InnerCoreModule.class.getName() + getDeclaringSourcePart(getClass()),
          asModuleChain(OuterCoreModule.class, InnerCoreModule.class));
    }
  }

  static class MissingParameter {
    @Inject MissingParameter(NoInjectConstructor noInjectConstructor) {}
  }

  static class NoInjectConstructor {
    private NoInjectConstructor() {}
  }

  @ProvidedBy(HasProvidedBy1Provider.class)
  interface HasProvidedBy1 {}

  static class HasProvidedBy1Provider implements Provider<HasProvidedBy1> {
    public HasProvidedBy1 get() {
      return new HasProvidedBy1() {};
    }
  }

  @ImplementedBy(ImplementsHasImplementedBy1.class)
  interface HasImplementedBy1 {}

  static class ImplementsHasImplementedBy1 implements HasImplementedBy1 {}

  @ProvidedBy(HasProvidedBy2Provider.class)
  static class HasProvidedBy2 {}

  static class HasProvidedBy2Provider implements Provider<HasProvidedBy2> {
    public HasProvidedBy2 get() {
      return new HasProvidedBy2() {};
    }
  }

  @ImplementedBy(ExtendsHasImplementedBy2.class)
  static class HasImplementedBy2 {}

  static class ExtendsHasImplementedBy2 extends HasImplementedBy2 {}

  static class JustAClass {}

  @ImplementedBy(ImplementsHasImplementedByThatNeedsAnotherImplementedBy.class)
  static interface HasImplementedByThatNeedsAnotherImplementedBy {
  }

  static class ImplementsHasImplementedByThatNeedsAnotherImplementedBy
    implements HasImplementedByThatNeedsAnotherImplementedBy {
    @Inject
    ImplementsHasImplementedByThatNeedsAnotherImplementedBy(
        HasImplementedBy1 h1n1) {}
  }

  @ImplementedBy(ImplementsHasImplementedByThatWantsExplicit.class)
  static interface HasImplementedByThatWantsExplicit {
  }

  static class ImplementsHasImplementedByThatWantsExplicit
      implements HasImplementedByThatWantsExplicit {
    @Inject ImplementsHasImplementedByThatWantsExplicit(JustAnInterface jai) {}
  }

  static interface JustAnInterface {}


//  public void testBindInterfaceWithoutImplementation() {
//    Guice.createInjector(new AbstractModule() {
//      protected void configure() {
//        bind(Runnable.class);
//      }
//    }).getInstance(Runnable.class);
//  }

  enum Roshambo { ROCK, SCISSORS, PAPER }

  public void testInjectRawProvider() {
    try {
      Guice.createInjector().getInstance(Provider.class);
      fail();
    } catch (ConfigurationException expected) {
      Asserts.assertContains(expected.getMessage(),
          "1) Cannot inject a Provider that has no type parameter",
          "while locating " + Provider.class.getName());
    }
  }
}
