/*
 * 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 com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.inject.internal.Annotations;
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 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;
import junit.framework.TestCase;

/** @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() {
          @Override
          public void configure(Binder binder) {
            fooProvider = binder.getProvider(Foo.class);

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

    assertNotNull(fooProvider.get());
  }

  static class Foo {}

  public void testMissingBindings() {
    try {
      Guice.createInjector(
          // We put each binding in a separate module so the order of the error messages doesn't
          // depend on line numbers
          new AbstractModule() {
            @Override
            public void configure() {
              getProvider(Runnable.class);
            }
          },
          new AbstractModule() {
            @Override
            public void configure() {
              bind(Comparator.class);
            }
          },
          new AbstractModule() {
            @Override
            public void configure() {
              requireBinding(Key.get(new TypeLiteral<Callable<String>>() {}));
            }
          },
          new AbstractModule() {
            @Override
            public void configure() {
              bind(Date.class).annotatedWith(Names.named("date"));
            }
          });
      fail("Expected CreationException");
    } catch (CreationException e) {
      assertEquals(4, e.getErrorMessages().size());
      String segment1 = "No implementation for java.lang.Runnable was bound.";
      String segment2 = "No implementation for " + Comparator.class.getName() + " was bound.";
      String segment3 =
          "No implementation for java.util.concurrent.Callable<java.lang.String> was" + " bound.";
      String segment4 =
          "No implementation for java.util.Date annotated with @"
              + Named.class.getName()
              + "(value="
              + Annotations.memberValueString("date")
              + ") was bound.";
      String atSegment = "at " + getClass().getName();
      String sourceFileName = getDeclaringSourcePart(getClass());
      assertContains(
          e.getMessage(),
          segment1,
          atSegment,
          sourceFileName,
          segment2,
          atSegment,
          sourceFileName,
          segment3,
          atSegment,
          sourceFileName,
          segment4,
          atSegment,
          sourceFileName);
    }
  }

  public void testMissingDependency() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            public void configure() {
              bind(NeedsRunnable.class);
            }
          });
      fail("Expected CreationException");
    } 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(),
          "for the 1st parameter of "
              + 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> {
    @Override
    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> {
    @Override
    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());
    }
  }
}
