/**
 * 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.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 junit.framework.TestCase;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @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() {
        @Override protected void configure() {}

        @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=a) was bound.",
          "for field at " + AB.class.getName() + ".a(PrivateModuleTest.java:",
          "3) No implementation for " + String.class.getName(), "Named(value=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() {
          @Override protected void configure() {}

          @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;

              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() {
          @Override protected void configure() {}

          @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;

              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;
  } 
}
