/*
 * Copyright (C) 2008 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.getDeclaringSourcePart;
import static com.google.inject.name.Names.named;

import com.google.common.collect.ImmutableSet;
import com.google.inject.internal.Annotations;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.ExposedBinding;
import com.google.inject.spi.PrivateElements;
import com.google.inject.util.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import junit.framework.TestCase;

/** @author jessewilson@google.com (Jesse Wilson) */
public class PrivateModuleTest extends TestCase {

  public void testBasicUsage() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(String.class).annotatedWith(named("a")).toInstance("public");

                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {
                        bind(String.class).annotatedWith(named("b")).toInstance("i");

                        bind(AB.class).annotatedWith(named("one")).to(AB.class);
                        expose(AB.class).annotatedWith(named("one"));
                      }
                    });

                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {
                        bind(String.class).annotatedWith(named("b")).toInstance("ii");

                        bind(AB.class).annotatedWith(named("two")).to(AB.class);
                        expose(AB.class).annotatedWith(named("two"));
                      }
                    });
              }
            });

    AB ab1 = injector.getInstance(Key.get(AB.class, named("one")));
    assertEquals("public", ab1.a);
    assertEquals("i", ab1.b);

    AB ab2 = injector.getInstance(Key.get(AB.class, named("two")));
    assertEquals("public", ab2.a);
    assertEquals("ii", ab2.b);
  }

  public void testWithoutPrivateModules() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                PrivateBinder bindA = binder().newPrivateBinder();
                bindA.bind(String.class).annotatedWith(named("a")).toInstance("i");
                bindA.expose(String.class).annotatedWith(named("a"));
                bindA.bind(String.class).annotatedWith(named("c")).toInstance("private to A");

                PrivateBinder bindB = binder().newPrivateBinder();
                bindB.bind(String.class).annotatedWith(named("b")).toInstance("ii");
                bindB.expose(String.class).annotatedWith(named("b"));
                bindB.bind(String.class).annotatedWith(named("c")).toInstance("private to B");
              }
            });

    assertEquals("i", injector.getInstance(Key.get(String.class, named("a"))));
    assertEquals("ii", injector.getInstance(Key.get(String.class, named("b"))));
  }

  public void testMisplacedExposedAnnotation() {
    try {
      Guice.createInjector(
          new AbstractModule() {

            @Provides
            @Exposed
            String provideString() {
              return "i";
            }
          });
      fail();
    } catch (CreationException expected) {
      assertContains(
          expected.getMessage(),
          "Cannot expose java.lang.String on a standard binder. ",
          "Exposed bindings are only applicable to private binders.",
          " at " + PrivateModuleTest.class.getName(),
          "provideString(PrivateModuleTest.java:");
    }
  }

  public void testMisplacedExposeStatement() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              ((PrivateBinder) binder()).expose(String.class).annotatedWith(named("a"));
            }
          });
      fail();
    } catch (CreationException expected) {
      assertContains(
          expected.getMessage(),
          "Cannot expose java.lang.String on a standard binder. ",
          "Exposed bindings are only applicable to private binders.",
          " at " + PrivateModuleTest.class.getName(),
          getDeclaringSourcePart(getClass()));
    }
  }

  public void testPrivateModulesAndProvidesMethods() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {
                        expose(String.class).annotatedWith(named("a"));
                      }

                      @Provides
                      @Named("a")
                      String providePublicA() {
                        return "i";
                      }

                      @Provides
                      @Named("b")
                      String providePrivateB() {
                        return "private";
                      }
                    });

                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {}

                      @Provides
                      @Named("c")
                      String providePrivateC() {
                        return "private";
                      }

                      @Provides
                      @Exposed
                      @Named("d")
                      String providePublicD() {
                        return "ii";
                      }
                    });
              }
            });

    assertEquals("i", injector.getInstance(Key.get(String.class, named("a"))));

    try {
      injector.getInstance(Key.get(String.class, named("b")));
      fail();
    } catch (ConfigurationException expected) {
    }

    try {
      injector.getInstance(Key.get(String.class, named("c")));
      fail();
    } catch (ConfigurationException expected) {
    }

    assertEquals("ii", injector.getInstance(Key.get(String.class, named("d"))));
  }

  public void testCannotBindAKeyExportedByASibling() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(
                  new PrivateModule() {
                    @Override
                    public void configure() {
                      bind(String.class).toInstance("public");
                      expose(String.class);
                    }
                  });

              install(
                  new PrivateModule() {
                    @Override
                    public void configure() {
                      bind(String.class).toInstance("private");
                    }
                  });
            }
          });
      fail();
    } catch (CreationException expected) {
      assertContains(
          expected.getMessage(),
          "A binding to java.lang.String was already configured at ",
          getClass().getName(),
          getDeclaringSourcePart(getClass()),
          " at " + getClass().getName(),
          getDeclaringSourcePart(getClass()));
    }
  }

  public void testExposeButNoBind() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(String.class).annotatedWith(named("a")).toInstance("a");
              bind(String.class).annotatedWith(named("b")).toInstance("b");

              install(
                  new PrivateModule() {
                    @Override
                    public void configure() {
                      expose(AB.class);
                    }
                  });
            }
          });
      fail("AB was exposed but not bound");
    } catch (CreationException expected) {
      assertContains(
          expected.getMessage(),
          "Could not expose() " + AB.class.getName() + ", it must be explicitly bound",
          getDeclaringSourcePart(getClass()));
    }
  }

  /**
   * Ensure that when we've got errors in different private modules, Guice presents all errors in a
   * unified message.
   */
  public void testMessagesFromPrivateModulesAreNicelyIntegrated() {
    try {
      Guice.createInjector(
          new PrivateModule() {
            @Override
            public void configure() {
              bind(C.class);
            }
          },
          new PrivateModule() {
            @Override
            public void configure() {
              bind(AB.class);
            }
          });
      fail();
    } catch (CreationException expected) {
      assertContains(
          expected.getMessage(),
          "1) No implementation for " + C.class.getName() + " was bound.",
          "at " + getClass().getName(),
          getDeclaringSourcePart(getClass()),
          "2) No implementation for " + String.class.getName(),
          "Named(value=" + Annotations.memberValueString("a") + ") was bound.",
          "for field at " + AB.class.getName() + ".a(PrivateModuleTest.java:",
          "3) No implementation for " + String.class.getName(),
          "Named(value=" + Annotations.memberValueString("b") + ") was bound.",
          "for field at " + AB.class.getName() + ".b(PrivateModuleTest.java:",
          "3 errors");
    }
  }

  public void testNestedPrivateInjectors() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              public void configure() {
                expose(String.class);

                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {
                        bind(String.class).toInstance("nested");
                        expose(String.class);
                      }
                    });
              }
            });

    assertEquals("nested", injector.getInstance(String.class));
  }

  public void testInstallingRegularModulesFromPrivateModules() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              public void configure() {
                expose(String.class);

                install(
                    new AbstractModule() {
                      @Override
                      protected void configure() {
                        bind(String.class).toInstance("nested");
                      }
                    });
              }
            });

    assertEquals("nested", injector.getInstance(String.class));
  }

  public void testNestedPrivateModulesWithSomeKeysUnexposed() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              public void configure() {
                bind(String.class)
                    .annotatedWith(named("bound outer, exposed outer"))
                    .toInstance("boeo");
                expose(String.class).annotatedWith(named("bound outer, exposed outer"));
                bind(String.class)
                    .annotatedWith(named("bound outer, exposed none"))
                    .toInstance("boen");
                expose(String.class).annotatedWith(named("bound inner, exposed both"));

                install(
                    new PrivateModule() {
                      @Override
                      public void configure() {
                        bind(String.class)
                            .annotatedWith(named("bound inner, exposed both"))
                            .toInstance("bieb");
                        expose(String.class).annotatedWith(named("bound inner, exposed both"));
                        bind(String.class)
                            .annotatedWith(named("bound inner, exposed none"))
                            .toInstance("bien");
                      }
                    });
              }
            });

    assertEquals(
        "boeo", injector.getInstance(Key.get(String.class, named("bound outer, exposed outer"))));
    assertEquals(
        "bieb", injector.getInstance(Key.get(String.class, named("bound inner, exposed both"))));

    try {
      injector.getInstance(Key.get(String.class, named("bound outer, exposed none")));
      fail();
    } catch (ConfigurationException expected) {
    }

    try {
      injector.getInstance(Key.get(String.class, named("bound inner, exposed none")));
      fail();
    } catch (ConfigurationException expected) {
    }
  }

  public void testDependenciesBetweenPrivateAndPublic() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              protected void configure() {}

              @Provides
              @Exposed
              @Named("a")
              String provideA() {
                return "A";
              }

              @Provides
              @Exposed
              @Named("abc")
              String provideAbc(@Named("ab") String ab) {
                return ab + "C";
              }
            },
            new AbstractModule() {

              @Provides
              @Named("ab")
              String provideAb(@Named("a") String a) {
                return a + "B";
              }

              @Provides
              @Named("abcd")
              String provideAbcd(@Named("abc") String abc) {
                return abc + "D";
              }
            });

    assertEquals("ABCD", injector.getInstance(Key.get(String.class, named("abcd"))));
  }

  public void testDependenciesBetweenPrivateAndPublicWithPublicEagerSingleton() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              protected void configure() {}

              @Provides
              @Exposed
              @Named("a")
              String provideA() {
                return "A";
              }

              @Provides
              @Exposed
              @Named("abc")
              String provideAbc(@Named("ab") String ab) {
                return ab + "C";
              }
            },
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(String.class)
                    .annotatedWith(named("abcde"))
                    .toProvider(
                        new Provider<String>() {
                          @Inject
                          @Named("abcd")
                          String abcd;

                          @Override
                          public String get() {
                            return abcd + "E";
                          }
                        })
                    .asEagerSingleton();
              }

              @Provides
              @Named("ab")
              String provideAb(@Named("a") String a) {
                return a + "B";
              }

              @Provides
              @Named("abcd")
              String provideAbcd(@Named("abc") String abc) {
                return abc + "D";
              }
            });

    assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde"))));
  }

  public void testDependenciesBetweenPrivateAndPublicWithPrivateEagerSingleton() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {

              @Provides
              @Named("ab")
              String provideAb(@Named("a") String a) {
                return a + "B";
              }

              @Provides
              @Named("abcd")
              String provideAbcd(@Named("abc") String abc) {
                return abc + "D";
              }
            },
            new PrivateModule() {
              @Override
              protected void configure() {
                bind(String.class)
                    .annotatedWith(named("abcde"))
                    .toProvider(
                        new Provider<String>() {
                          @Inject
                          @Named("abcd")
                          String abcd;

                          @Override
                          public String get() {
                            return abcd + "E";
                          }
                        })
                    .asEagerSingleton();
                expose(String.class).annotatedWith(named("abcde"));
              }

              @Provides
              @Exposed
              @Named("a")
              String provideA() {
                return "A";
              }

              @Provides
              @Exposed
              @Named("abc")
              String provideAbc(@Named("ab") String ab) {
                return ab + "C";
              }
            });

    assertEquals("ABCDE", injector.getInstance(Key.get(String.class, named("abcde"))));
  }

  static class AB {
    @Inject
    @Named("a")
    String a;

    @Inject
    @Named("b")
    String b;
  }

  interface C {}

  public void testSpiAccess() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              public void configure() {
                bind(String.class).annotatedWith(named("a")).toInstance("private");
                bind(String.class).annotatedWith(named("b")).toInstance("exposed");
                expose(String.class).annotatedWith(named("b"));
              }
            });

    ExposedBinding<?> binding =
        (ExposedBinding<?>) injector.getBinding(Key.get(String.class, Names.named("b")));
    assertEquals(
        ImmutableSet.<Dependency<?>>of(Dependency.get(Key.get(Injector.class))),
        binding.getDependencies());
    PrivateElements privateElements = binding.getPrivateElements();
    assertEquals(
        ImmutableSet.<Key<?>>of(Key.get(String.class, named("b"))),
        privateElements.getExposedKeys());
    assertContains(
        privateElements.getExposedSource(Key.get(String.class, named("b"))).toString(),
        PrivateModuleTest.class.getName(),
        getDeclaringSourcePart(getClass()));
    Injector privateInjector = privateElements.getInjector();
    assertEquals("private", privateInjector.getInstance(Key.get(String.class, Names.named("a"))));
  }

  public void testParentBindsSomethingInPrivate() {
    try {
      Guice.createInjector(new FailingModule());
      fail();
    } catch (CreationException expected) {
      assertEquals(1, expected.getErrorMessages().size());
      assertContains(
          expected.toString(),
          "Unable to create binding for java.util.List.",
          "It was already configured on one or more child injectors or private modules",
          "bound at " + FailingPrivateModule.class.getName() + ".configure(",
          asModuleChain(FailingModule.class, ManyPrivateModules.class, FailingPrivateModule.class),
          "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(",
          asModuleChain(
              FailingModule.class, ManyPrivateModules.class, SecondFailingPrivateModule.class),
          "If it was in a PrivateModule, did you forget to expose the binding?",
          "at " + FailingModule.class.getName() + ".configure(");
    }
  }

  public void testParentBindingToPrivateLinkedJitBinding() {
    Injector injector = Guice.createInjector(new ManyPrivateModules());
    try {
      injector.getBinding(Key.get(Types.providerOf(List.class)));
      fail();
    } catch (ConfigurationException expected) {
      assertEquals(1, expected.getErrorMessages().size());
      assertContains(
          expected.toString(),
          "Unable to create binding for com.google.inject.Provider<java.util.List>.",
          "It was already configured on one or more child injectors or private modules",
          "bound at " + FailingPrivateModule.class.getName() + ".configure(",
          asModuleChain(ManyPrivateModules.class, FailingPrivateModule.class),
          "bound at " + SecondFailingPrivateModule.class.getName() + ".configure(",
          asModuleChain(ManyPrivateModules.class, SecondFailingPrivateModule.class),
          "If it was in a PrivateModule, did you forget to expose the binding?",
          "while locating com.google.inject.Provider<java.util.List>");
    }
  }

  public void testParentBindingToPrivateJitBinding() {
    Injector injector = Guice.createInjector(new ManyPrivateModules());
    try {
      injector.getBinding(PrivateFoo.class);
      fail();
    } catch (ConfigurationException expected) {
      assertEquals(1, expected.getErrorMessages().size());
      assertContains(
          expected.toString(),
          "Unable to create binding for " + PrivateFoo.class.getName(),
          "It was already configured on one or more child injectors or private modules",
          "(bound by a just-in-time binding)",
          "If it was in a PrivateModule, did you forget to expose the binding?",
          "while locating " + PrivateFoo.class.getName());
    }
  }

  private static class FailingModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(Collection.class).to(List.class);
      install(new ManyPrivateModules());
    }
  }

  private static class ManyPrivateModules extends AbstractModule {
    @Override
    protected void configure() {
      // make sure duplicate sources are collapsed
      install(new FailingPrivateModule());
      install(new FailingPrivateModule());
      // but additional sources are listed
      install(new SecondFailingPrivateModule());
    }
  }

  private static class FailingPrivateModule extends PrivateModule {
    @Override
    protected void configure() {
      bind(List.class).toInstance(new ArrayList());

      // Add the Provider<List> binding, created just-in-time,
      // to make sure our linked JIT bindings have the correct source.
      getProvider(Key.get(Types.providerOf(List.class)));

      // Request a JIT binding for PrivateFoo, which can only
      // be created in the private module because it depends
      // on List.
      getProvider(PrivateFoo.class);
    }
  }

  /** A second class, so we can see another name in the source list. */
  private static class SecondFailingPrivateModule extends PrivateModule {
    @Override
    protected void configure() {
      bind(List.class).toInstance(new ArrayList());

      // Add the Provider<List> binding, created just-in-time,
      // to make sure our linked JIT bindings have the correct source.
      getProvider(Key.get(Types.providerOf(List.class)));

      // Request a JIT binding for PrivateFoo, which can only
      // be created in the private module because it depends
      // on List.
      getProvider(PrivateFoo.class);
    }
  }

  private static class PrivateFoo {
    @Inject List list;
  }
}
