/*
 * Copyright (C) 2010 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.common.collect.ImmutableSet.of;
import static com.google.common.truth.Truth.assertThat;
import static com.google.inject.Asserts.assertContains;
import static com.google.inject.JitBindingsTest.GetBindingCheck.ALLOW_BINDING;
import static com.google.inject.JitBindingsTest.GetBindingCheck.FAIL_ALL;

import java.util.Set;
import junit.framework.TestCase;

/**
 * Some tests for {@link Binder#requireExplicitBindings()}
 *
 * @author sberlin@gmail.com (Sam Berlin)
 */
public class JitBindingsTest extends TestCase {

  private String jitFailed(Class<?> clazz) {
    return jitFailed(TypeLiteral.get(clazz));
  }

  private String jitFailed(TypeLiteral<?> clazz) {
    String shortName = "JitBindingsTest$" + clazz.getRawType().getSimpleName();
    return "Explicit bindings are required and " + shortName + " is not explicitly bound.";
  }

  private String jitInParentFailed(Class<?> clazz) {
    return jitInParentFailed(TypeLiteral.get(clazz));
  }

  private String jitInParentFailed(TypeLiteral<?> clazz) {
    String shortName = "JitBindingsTest$" + clazz.getRawType().getSimpleName();
    return "Explicit bindings are required and "
        + shortName
        + " would be bound in a parent injector.";
  }

  private String inChildMessage(Class<?> clazz) {
    return "Unable to create binding for "
        + "JitBindingsTest$"
        + clazz.getSimpleName()
        + " because it was already configured on one or more child injectors or private modules";
  }

  public void testLinkedBindingWorks() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).to(FooImpl.class);
              }
            });
    // Foo was explicitly bound
    ensureWorks(injector, Foo.class);
    // FooImpl was implicitly bound, it is an error to call getInstance or getProvider,
    // It is OK to call getBinding for introspection, but an error to get the provider
    // of the binding
    ensureFails(injector, ALLOW_BINDING, FooImpl.class);
  }

  public void testMoreBasicsWork() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).to(FooImpl.class);
                bind(Bar.class);
                bind(FooBar.class);
              }
            });
    // Foo, Bar & FooBar was explicitly bound
    ensureWorks(injector, FooBar.class, Bar.class, Foo.class);
    // FooImpl was implicitly bound, it is an error to call getInstance or getProvider,
    // It is OK to call getBinding for introspection, but an error to get the provider
    // of the binding
    ensureFails(injector, ALLOW_BINDING, FooImpl.class);
  }

  public void testLinkedEagerSingleton() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).to(FooImpl.class).asEagerSingleton();
              }
            });
    // Foo was explicitly bound
    ensureWorks(injector, Foo.class);
    // FooImpl was implicitly bound, it is an error to call getInstance or getProvider,
    // It is OK to call getBinding for introspection, but an error to get the provider
    // of the binding
    ensureFails(injector, ALLOW_BINDING, FooImpl.class);
  }

  public void testBasicsWithEagerSingleton() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).to(FooImpl.class).asEagerSingleton();
                bind(Bar.class);
                bind(FooBar.class);
              }
            });
    // Foo, Bar & FooBar was explicitly bound
    ensureWorks(injector, FooBar.class, Bar.class, Foo.class);
    // FooImpl was implicitly bound, it is an error to call getInstance or getProvider,
    // It is OK to call getBinding for introspection, but an error to get the provider
    // of the binding
    ensureFails(injector, ALLOW_BINDING, FooImpl.class);
  }

  public void testLinkedToScoped() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder.requireExplicitBindings();
                bind(Foo.class).to(ScopedFooImpl.class);
              }
            });
    // Foo was explicitly bound
    ensureWorks(injector, Foo.class);
    // FooSingletonImpl was implicitly bound, it is an error to call getInstance or getProvider,
    // It is OK to call getBinding for introspection, but an error to get the provider
    // of the binding
    ensureFails(injector, ALLOW_BINDING, ScopedFooImpl.class);
  }

  public void testBasicsWithScoped() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).to(ScopedFooImpl.class);
                bind(Bar.class);
                bind(FooBar.class);
              }
            });
    // Foo, Bar & FooBar was explicitly bound
    ensureWorks(injector, FooBar.class, Bar.class, Foo.class);
    // FooSingletonImpl was implicitly bound, it is an error to call getInstance or getProvider,
    // It is OK to call getBinding for introspection, but an error to get the provider
    // of the binding
    ensureFails(injector, ALLOW_BINDING, ScopedFooImpl.class);
  }

  public void testFailsIfInjectingScopedDirectlyWhenItIsntBound() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              binder().requireExplicitBindings();
              bind(Foo.class).to(ScopedFooImpl.class);
              bind(WantsScopedFooImpl.class);
            }
          });
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(ScopedFooImpl.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testLinkedProviderBindingWorks() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).toProvider(FooProvider.class);
              }
            });
    // Foo was explicitly bound
    ensureWorks(injector, Foo.class);
    // FooImpl was not bound at all (even implicitly), it is an error
    // to call getInstance, getProvider, or getBinding.
    ensureFails(injector, FAIL_ALL, FooImpl.class);
  }

  public void testJitGetFails() {
    try {
      Guice.createInjector(
              new AbstractModule() {
                @Override
                protected void configure() {
                  binder().requireExplicitBindings();
                }
              })
          .getInstance(Bar.class);
      fail("should have failed");
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(), jitFailed(Bar.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testJitInjectionFails() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              binder().requireExplicitBindings();
              bind(Foo.class).to(FooImpl.class);
              bind(FooBar.class);
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(Bar.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testJitProviderGetFails() {
    try {
      Guice.createInjector(
              new AbstractModule() {
                @Override
                protected void configure() {
                  binder().requireExplicitBindings();
                }
              })
          .getProvider(Bar.class);
      fail("should have failed");
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(), jitFailed(Bar.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testJitProviderInjectionFails() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              binder().requireExplicitBindings();
              bind(Foo.class).to(FooImpl.class);
              bind(ProviderFooBar.class);
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(Bar.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testImplementedBy() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(ImplBy.class);
              }
            });
    ensureWorks(injector, ImplBy.class);
    ensureFails(injector, ALLOW_BINDING, ImplByImpl.class);
  }

  public void testImplementedBySomethingThatIsAnnotated() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(ImplByScoped.class);
              }
            });
    ensureWorks(injector, ImplByScoped.class);
    ensureFails(injector, ALLOW_BINDING, ImplByScopedImpl.class);
  }

  public void testProvidedBy() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(ProvBy.class);
              }
            });
    ensureWorks(injector, ProvBy.class);
    ensureFails(injector, ALLOW_BINDING, ProvByProvider.class);
  }

  public void testProviderMethods() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
              }

              @SuppressWarnings("unused")
              @Provides
              Foo foo() {
                return new FooImpl();
              }
            });
    ensureWorks(injector, Foo.class);
  }

  public void testChildInjectorInheritsOption() {
    Injector parent =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Bar.class);
              }
            });
    ensureWorks(parent, Bar.class);
    ensureFails(parent, FAIL_ALL, FooImpl.class, FooBar.class, Foo.class);

    try {
      parent.createChildInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(FooBar.class);
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(Foo.class));
      assertEquals(1, expected.getErrorMessages().size());
    }

    Injector child =
        parent.createChildInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(Foo.class).to(FooImpl.class);
              }
            });
    ensureWorks(child, Foo.class, Bar.class);
    ensureFails(child, ALLOW_BINDING, FooImpl.class);
    ensureInChild(parent, FooImpl.class, Foo.class);
    // TODO(sameb): FooBar may or may not be in a child injector, depending on if GC has run.
    // We should fix failed child injectors to remove their contents from the parent banned list
    // immediately, rather than waiting on GC to do it.
    // FooBar was succesfully inserted into the child injector (and parent banned list), but then
    // JIT bindings it depended on failed, making the child injector invalid.

    Injector grandchild =
        child.createChildInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(FooBar.class);
              }
            });
    ensureWorks(grandchild, FooBar.class, Foo.class, Bar.class);
    ensureFails(grandchild, ALLOW_BINDING, FooImpl.class);
    ensureFails(child, ALLOW_BINDING, FooImpl.class);
    ensureInChild(parent, FooImpl.class, FooBar.class, Foo.class);
  }

  public void testChildInjectorAddsOption() {
    Injector parent =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(Bar.class);
              }
            });
    int totalParentBindings = parent.getAllBindings().size();

    try {
      parent.createChildInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              binder().requireExplicitBindings();
              bind(FooBar.class);
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(Foo.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
    assertEquals(totalParentBindings, parent.getAllBindings().size());

    Injector child =
        parent.createChildInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(Foo.class).to(FooImpl.class);
                bind(FooImpl.class);
              }
            });
    assertEquals(totalParentBindings, parent.getAllBindings().size());
    ensureWorks(child, Foo.class, Bar.class);

    Injector grandchild =
        child.createChildInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(FooBar.class);
              }
            });
    assertEquals(totalParentBindings, parent.getAllBindings().size());
    ensureWorks(grandchild, FooBar.class, Foo.class, Bar.class);

    // Make sure siblings of children don't inherit each others settings...
    // a new child should be able to get FooImpl.
    child = parent.createChildInjector();
    ensureWorks(child, FooImpl.class);
  }

  public void testPrivateModulesInheritOptions() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              binder().requireExplicitBindings();
              bind(Foo.class).to(FooImpl.class);

              install(
                  new PrivateModule() {
                    @Override
                    public void configure() {
                      bind(FooBar.class);
                      expose(FooBar.class);
                    }
                  });
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(Bar.class));
      assertEquals(1, expected.getErrorMessages().size());
    }

    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();

                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {
                        bind(Foo.class).to(FooImpl.class);
                        expose(Foo.class);
                      }
                    });
              }
            });
    ensureInChild(injector, FooImpl.class);
  }

  public void testPrivateModuleAddsOption() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(Foo.class).to(FooImpl.class);

              // Fails because FooBar is in the private module,
              // and it wants Bar, but Bar would be JIT.
              install(
                  new PrivateModule() {
                    @Override
                    public void configure() {
                      binder().requireExplicitBindings();
                      bind(FooBar.class);
                      expose(FooBar.class);
                    }
                  });
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitFailed(Bar.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testPrivateModuleSiblingsDontShareOption() {
    Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(Foo.class).to(FooImpl.class);

            install(
                new PrivateModule() {
                  @Override
                  public void configure() {
                    binder().requireExplicitBindings();
                  }
                });

            // This works, even though Bar is JIT,
            // because the requireExplicitBindings isn't shared
            // between sibling private modules.
            install(
                new PrivateModule() {
                  @Override
                  public void configure() {
                    bind(FooBar.class);
                    expose(FooBar.class);
                  }
                });
          }
        });
  }

  public void testTypeLiteralsCanBeInjected() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
                bind(new TypeLiteral<WantsTypeLiterals<String>>() {});
                bind(new TypeLiteral<Set<String>>() {}).toInstance(of("bar"));
              }
            });

    WantsTypeLiterals<String> foo = injector.getInstance(new Key<WantsTypeLiterals<String>>() {});
    assertEquals(foo.literal.getRawType(), String.class);
    assertEquals(of("bar"), foo.set);
  }

  public void testMembersInjectorsCanBeInjected() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();
              }

              @Provides
              String data(MembersInjector<String> mi) {
                String data = "foo";
                mi.injectMembers(data);
                return data;
              }
            });

    String data = injector.getInstance(String.class);
    assertEquals("foo", data);
  }

  public void testJitLinkedBindingInParentFails() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(
                  new PrivateModule() {
                    @Override
                    protected void configure() {
                      binder().requireExplicitBindings();
                      bind(Foo.class).to(FooImpl.class);
                    }
                  });
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitInParentFailed(FooImpl.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testJitProviderBindingInParentFails() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(
                  new PrivateModule() {
                    @Override
                    protected void configure() {
                      binder().requireExplicitBindings();
                      bind(Foo.class).toProvider(FooProvider.class);
                    }
                  });
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitInParentFailed(FooProvider.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testJitImplementedByBindingInParentFails() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(
                  new PrivateModule() {
                    @Override
                    protected void configure() {
                      binder().requireExplicitBindings();
                      bind(ImplBy.class);
                    }
                  });
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitInParentFailed(ImplByImpl.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  public void testJitProvidedByBindingInParentFails() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(
                  new PrivateModule() {
                    @Override
                    protected void configure() {
                      binder().requireExplicitBindings();
                      bind(ProvBy.class);
                    }
                  });
            }
          });
      fail("should have failed");
    } catch (CreationException expected) {
      assertContains(expected.getMessage(), jitInParentFailed(ProvByProvider.class));
      assertEquals(1, expected.getErrorMessages().size());
    }
  }

  @ImplementedBy(AImpl.class)
  static interface A {}

  static class AImpl implements A {}

  @ImplementedBy(BImpl.class)
  static interface B {}

  static class BImpl implements B {
    @Inject
    BImpl(A a) {}
  }

  // See https://github.com/google/guice/issues/700
  public void testOutOfOrderImplementedByWorks() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder().requireExplicitBindings();

                bind(B.class);
                bind(A.class);
              }
            });
    assertThat(injector.getInstance(B.class)).isNotNull();
  }

  private void ensureWorks(Injector injector, Class<?>... classes) {
    for (int i = 0; i < classes.length; i++) {
      injector.getInstance(classes[i]);
      injector.getProvider(classes[i]).get();
      injector.getBinding(classes[i]).getProvider().get();
    }
  }

  enum GetBindingCheck {
    FAIL_ALL,
    ALLOW_BINDING,
    ALLOW_BINDING_PROVIDER
  }

  private void ensureFails(Injector injector, GetBindingCheck getBinding, Class<?>... classes) {
    for (int i = 0; i < classes.length; i++) {
      try {
        injector.getInstance(classes[i]);
        fail("should have failed tring to retrieve class: " + classes[i]);
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), jitFailed(classes[i]));
        assertEquals(1, expected.getErrorMessages().size());
      }

      try {
        injector.getProvider(classes[i]);
        fail("should have failed tring to retrieve class: " + classes[i]);
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), jitFailed(classes[i]));
        assertEquals(1, expected.getErrorMessages().size());
      }

      if (getBinding == GetBindingCheck.ALLOW_BINDING
          || getBinding == GetBindingCheck.ALLOW_BINDING_PROVIDER) {
        Binding<?> binding = injector.getBinding(classes[i]);
        try {
          binding.getProvider();
          if (getBinding != GetBindingCheck.ALLOW_BINDING_PROVIDER) {
            fail("should have failed trying to retrieve class: " + classes[i]);
          }
        } catch (ConfigurationException expected) {
          if (getBinding == GetBindingCheck.ALLOW_BINDING_PROVIDER) {
            throw expected;
          }
          assertContains(expected.getMessage(), jitFailed(classes[i]));
          assertEquals(1, expected.getErrorMessages().size());
        }
      } else {
        try {
          injector.getBinding(classes[i]);
          fail("should have failed tring to retrieve class: " + classes[i]);
        } catch (ConfigurationException expected) {
          assertContains(expected.getMessage(), jitFailed(classes[i]));
          assertEquals(1, expected.getErrorMessages().size());
        }
      }
    }
  }

  private void ensureInChild(Injector injector, Class<?>... classes) {
    for (int i = 0; i < classes.length; i++) {
      try {
        injector.getInstance(classes[i]);
        fail("should have failed tring to retrieve class: " + classes[i]);
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), inChildMessage(classes[i]));
        assertEquals(1, expected.getErrorMessages().size());
      }

      try {
        injector.getProvider(classes[i]);
        fail("should have failed tring to retrieve class: " + classes[i]);
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), inChildMessage(classes[i]));
        assertEquals(1, expected.getErrorMessages().size());
      }

      try {
        injector.getBinding(classes[i]);
        fail("should have failed tring to retrieve class: " + classes[i]);
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), inChildMessage(classes[i]));
        assertEquals(1, expected.getErrorMessages().size());
      }
    }
  }

  private static interface Foo {}

  private static class FooImpl implements Foo {}

  @Singleton
  private static class ScopedFooImpl implements Foo {}

  private static class WantsScopedFooImpl {
    @SuppressWarnings("unused")
    @Inject
    ScopedFooImpl scopedFoo;
  }

  private static class Bar {}

  private static class FooBar {
    @SuppressWarnings("unused")
    @Inject
    Foo foo;

    @SuppressWarnings("unused")
    @Inject
    Bar bar;
  }

  private static class ProviderFooBar {
    @SuppressWarnings("unused")
    @Inject
    Provider<Foo> foo;

    @SuppressWarnings("unused")
    @Inject
    Provider<Bar> bar;
  }

  private static class FooProvider implements Provider<Foo> {
    @Override
    public Foo get() {
      return new FooImpl();
    }
  }

  @ImplementedBy(ImplByImpl.class)
  private static interface ImplBy {}

  private static class ImplByImpl implements ImplBy {}

  @ImplementedBy(ImplByScopedImpl.class)
  private static interface ImplByScoped {}

  @Singleton
  private static class ImplByScopedImpl implements ImplByScoped {}

  @ProvidedBy(ProvByProvider.class)
  private static interface ProvBy {}

  private static class ProvByProvider implements Provider<ProvBy> {
    @Override
    public ProvBy get() {
      return new ProvBy() {};
    }
  }

  private static class WantsTypeLiterals<T> {
    TypeLiteral<T> literal;
    Set<T> set;

    @Inject
    WantsTypeLiterals(TypeLiteral<T> literal, Set<T> set) {
      this.literal = literal;
      this.set = set;
    }
  }
}
