/*
 * Copyright (C) 2009 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.assistedinject;

import static com.google.inject.Asserts.assertContains;
import static com.google.inject.name.Names.named;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.Message;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import junit.framework.TestCase;

public class FactoryModuleBuilderTest extends TestCase {

  private enum Color {
    BLUE,
    GREEN,
    RED,
    GRAY,
    BLACK
  }

  public void testImplicitForwardingAssistedBindingFailsWithInterface() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(Car.class).to(Golf.class);
              install(new FactoryModuleBuilder().build(ColoredCarFactory.class));
            }
          });
      fail();
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "1) " + Car.class.getName() + " is an interface, not a concrete class.",
          "Unable to create AssistedInject factory.",
          "while locating " + Car.class.getName(),
          "at " + ColoredCarFactory.class.getName() + ".create(");
      assertEquals(1, ce.getErrorMessages().size());
    }
  }

  public void testImplicitForwardingAssistedBindingFailsWithAbstractClass() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(AbstractCar.class).to(ArtCar.class);
              install(new FactoryModuleBuilder().build(ColoredAbstractCarFactory.class));
            }
          });
      fail();
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "1) " + AbstractCar.class.getName() + " is abstract, not a concrete class.",
          "Unable to create AssistedInject factory.",
          "while locating " + AbstractCar.class.getName(),
          "at " + ColoredAbstractCarFactory.class.getName() + ".create(");
      assertEquals(1, ce.getErrorMessages().size());
    }
  }

  public void testImplicitForwardingAssistedBindingCreatesNewObjects() {
    final Mustang providedMustang = new Mustang(Color.BLUE);
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(new FactoryModuleBuilder().build(MustangFactory.class));
              }

              @Provides
              Mustang provide() {
                return providedMustang;
              }
            });
    assertSame(providedMustang, injector.getInstance(Mustang.class));
    MustangFactory factory = injector.getInstance(MustangFactory.class);
    Mustang created = factory.create(Color.GREEN);
    assertNotSame(providedMustang, created);
    assertEquals(Color.BLUE, providedMustang.color);
    assertEquals(Color.GREEN, created.color);
  }

  public void testExplicitForwardingAssistedBindingFailsWithInterface() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(Volkswagen.class).to(Golf.class);
              install(
                  new FactoryModuleBuilder()
                      .implement(Car.class, Volkswagen.class)
                      .build(ColoredCarFactory.class));
            }
          });
      fail();
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "1) " + Volkswagen.class.getName() + " is an interface, not a concrete class.",
          "Unable to create AssistedInject factory.",
          "while locating " + Volkswagen.class.getName(),
          "while locating " + Car.class.getName(),
          "at " + ColoredCarFactory.class.getName() + ".create(");
      assertEquals(1, ce.getErrorMessages().size());
    }
  }

  public void testExplicitForwardingAssistedBindingFailsWithAbstractClass() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              bind(AbstractCar.class).to(ArtCar.class);
              install(
                  new FactoryModuleBuilder()
                      .implement(Car.class, AbstractCar.class)
                      .build(ColoredCarFactory.class));
            }
          });
      fail();
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "1) " + AbstractCar.class.getName() + " is abstract, not a concrete class.",
          "Unable to create AssistedInject factory.",
          "while locating " + AbstractCar.class.getName(),
          "while locating " + Car.class.getName(),
          "at " + ColoredCarFactory.class.getName() + ".create(");
      assertEquals(1, ce.getErrorMessages().size());
    }
  }

  public void testExplicitForwardingAssistedBindingCreatesNewObjects() {
    final Mustang providedMustang = new Mustang(Color.BLUE);
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Mustang.class)
                        .build(ColoredCarFactory.class));
              }

              @Provides
              Mustang provide() {
                return providedMustang;
              }
            });
    assertSame(providedMustang, injector.getInstance(Mustang.class));
    ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class);
    Mustang created = (Mustang) factory.create(Color.GREEN);
    assertNotSame(providedMustang, created);
    assertEquals(Color.BLUE, providedMustang.color);
    assertEquals(Color.GREEN, created.color);
  }

  public void testAnnotatedAndParentBoundReturnValue() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(Car.class).to(Golf.class);

                bind(Integer.class).toInstance(911);
                bind(Double.class).toInstance(5.0d);
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Names.named("german"), Beetle.class)
                        .implement(Car.class, Names.named("american"), Mustang.class)
                        .build(AnnotatedVersatileCarFactory.class));
              }
            });

    AnnotatedVersatileCarFactory factory = injector.getInstance(AnnotatedVersatileCarFactory.class);
    assertTrue(factory.getGermanCar(Color.BLACK) instanceof Beetle);
    assertTrue(injector.getInstance(Car.class) instanceof Golf);
  }

  public void testParentBoundReturnValue() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(Car.class).to(Golf.class);
                bind(Double.class).toInstance(5.0d);
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Mustang.class)
                        .build(ColoredCarFactory.class));
              }
            });

    ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class);
    assertTrue(factory.create(Color.RED) instanceof Mustang);
    assertTrue(injector.getInstance(Car.class) instanceof Golf);
  }

  public void testConfigureAnnotatedReturnValue() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Names.named("german"), Beetle.class)
                        .implement(Car.class, Names.named("american"), Mustang.class)
                        .build(AnnotatedVersatileCarFactory.class));
              }
            });

    AnnotatedVersatileCarFactory factory = injector.getInstance(AnnotatedVersatileCarFactory.class);
    assertTrue(factory.getGermanCar(Color.GRAY) instanceof Beetle);
    assertTrue(factory.getAmericanCar(Color.BLACK) instanceof Mustang);
  }

  public void testNoBindingAssistedInject() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(new FactoryModuleBuilder().build(MustangFactory.class));
              }
            });

    MustangFactory factory = injector.getInstance(MustangFactory.class);

    Mustang mustang = factory.create(Color.BLUE);
    assertEquals(Color.BLUE, mustang.color);
  }

  public void testBindingAssistedInject() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Mustang.class)
                        .build(ColoredCarFactory.class));
              }
            });

    ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class);

    Mustang mustang = (Mustang) factory.create(Color.BLUE);
    assertEquals(Color.BLUE, mustang.color);
  }

  public void testDuplicateBindings() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Mustang.class)
                        .build(ColoredCarFactory.class));
                install(
                    new FactoryModuleBuilder()
                        .implement(Car.class, Mustang.class)
                        .build(ColoredCarFactory.class));
              }
            });

    ColoredCarFactory factory = injector.getInstance(ColoredCarFactory.class);

    Mustang mustang = (Mustang) factory.create(Color.BLUE);
    assertEquals(Color.BLUE, mustang.color);
  }

  public void testSimilarBindingsWithConflictingImplementations() {
    try {
      Injector injector =
          Guice.createInjector(
              new AbstractModule() {
                @Override
                protected void configure() {
                  install(
                      new FactoryModuleBuilder()
                          .implement(Car.class, Mustang.class)
                          .build(ColoredCarFactory.class));
                  install(
                      new FactoryModuleBuilder()
                          .implement(Car.class, Golf.class)
                          .build(ColoredCarFactory.class));
                }
              });
      injector.getInstance(ColoredCarFactory.class);
      fail();
    } catch (CreationException ce) {
      assertContains(
          ce.getMessage(),
          "A binding to " + ColoredCarFactory.class.getName() + " was already configured");
      assertEquals(1, ce.getErrorMessages().size());
    }
  }

  public void testMultipleReturnTypes() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                bind(Double.class).toInstance(5.0d);
                install(new FactoryModuleBuilder().build(VersatileCarFactory.class));
              }
            });

    VersatileCarFactory factory = injector.getInstance(VersatileCarFactory.class);

    Mustang mustang = factory.getMustang(Color.RED);
    assertEquals(Color.RED, mustang.color);

    Beetle beetle = factory.getBeetle(Color.GREEN);
    assertEquals(Color.GREEN, beetle.color);
  }

  public void testParameterizedClassesWithNoImplements() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                install(
                    new FactoryModuleBuilder().build(new TypeLiteral<Foo.Factory<String>>() {}));
              }
            });

    Foo.Factory<String> factory =
        injector.getInstance(Key.get(new TypeLiteral<Foo.Factory<String>>() {}));
    @SuppressWarnings("unused")
    Foo<String> foo = factory.create(new Bar());
  }

  public void testGenericErrorMessageMakesSense() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(new FactoryModuleBuilder().build(Key.get(Foo.Factory.class)));
            }
          });
      fail();
    } catch (CreationException ce) {
      // Assert not only that it's the correct message, but also that it's the *only* message.
      Collection<Message> messages = ce.getErrorMessages();
      assertEquals(
          Foo.Factory.class.getName() + " cannot be used as a key; It is not fully specified.",
          Iterables.getOnlyElement(messages).getMessage());
    }
  }

  interface Car {}

  interface Volkswagen extends Car {}

  interface ColoredCarFactory {
    Car create(Color color);
  }

  interface MustangFactory {
    Mustang create(Color color);
  }

  interface VersatileCarFactory {
    Mustang getMustang(Color color);

    Beetle getBeetle(Color color);
  }

  interface AnnotatedVersatileCarFactory {
    @Named("german")
    Car getGermanCar(Color color);

    @Named("american")
    Car getAmericanCar(Color color);
  }

  public static class Golf implements Volkswagen {}

  public static class Mustang implements Car {
    private final Color color;

    @Inject
    public Mustang(@Assisted Color color) {
      this.color = color;
    }
  }

  public static class Beetle implements Car {
    private final Color color;

    @Inject
    public Beetle(@Assisted Color color) {
      this.color = color;
    }
  }

  public static class Foo<E> {
    static interface Factory<E> {
      Foo<E> create(Bar bar);
    }

    @SuppressWarnings("unused")
    @Inject
    Foo(@Assisted Bar bar, Baz<E> baz) {}
  }

  public static class Bar {}

  @SuppressWarnings("unused")
  public static class Baz<E> {}

  abstract static class AbstractCar implements Car {}

  interface ColoredAbstractCarFactory {
    AbstractCar create(Color color);
  }

  public static class ArtCar extends AbstractCar {}

  public void testFactoryBindingDependencies() {
    // validate dependencies work in all stages & as a raw element,
    // and that dependencies work for methods, fields, constructors,
    // and for @AssistedInject constructors too.
    Module module =
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(Integer.class).toInstance(42);
            bind(Double.class).toInstance(4.2d);
            bind(Float.class).toInstance(4.2f);
            bind(String.class).annotatedWith(named("dog")).toInstance("dog");
            bind(String.class).annotatedWith(named("cat1")).toInstance("cat1");
            bind(String.class).annotatedWith(named("cat2")).toInstance("cat2");
            bind(String.class).annotatedWith(named("cat3")).toInstance("cat3");
            bind(String.class).annotatedWith(named("arbitrary")).toInstance("fail!");
            install(
                new FactoryModuleBuilder()
                    .implement(Animal.class, Dog.class)
                    .build(AnimalHouse.class));
          }
        };

    Set<Key<?>> expectedKeys =
        ImmutableSet.<Key<?>>of(
            Key.get(Integer.class),
            Key.get(Double.class),
            Key.get(Float.class),
            Key.get(String.class, named("dog")),
            Key.get(String.class, named("cat1")),
            Key.get(String.class, named("cat2")),
            Key.get(String.class, named("cat3")));

    Injector injector = Guice.createInjector(module);
    validateDependencies(expectedKeys, injector.getBinding(AnimalHouse.class));

    injector = Guice.createInjector(Stage.TOOL, module);
    validateDependencies(expectedKeys, injector.getBinding(AnimalHouse.class));

    List<Element> elements = Elements.getElements(module);
    boolean found = false;
    for (Element element : elements) {
      if (element instanceof Binding) {
        Binding<?> binding = (Binding<?>) element;
        if (binding.getKey().equals(Key.get(AnimalHouse.class))) {
          found = true;
          validateDependencies(expectedKeys, binding);
          break;
        }
      }
    }
    assertTrue(found);
  }

  private void validateDependencies(Set<Key<?>> expectedKeys, Binding<?> binding) {
    Set<Dependency<?>> dependencies = ((HasDependencies) binding).getDependencies();
    Set<Key<?>> actualKeys = new HashSet<>();
    for (Dependency<?> dependency : dependencies) {
      actualKeys.add(dependency.getKey());
    }
    assertEquals(expectedKeys, actualKeys);
  }

  interface AnimalHouse {
    Animal createAnimal(String name);

    Cat createCat(String name);

    Cat createCat(int age);
  }

  interface Animal {}

  @SuppressWarnings("unused")
  private static class Dog implements Animal {
    @Inject int a;

    @Inject
    Dog(@Assisted String a, double b) {}

    @Inject
    void register(@Named("dog") String a) {}
  }

  @SuppressWarnings("unused")
  private static class Cat implements Animal {
    @Inject float a;

    @AssistedInject
    Cat(@Assisted String a, @Named("cat1") String b) {}

    @AssistedInject
    Cat(@Assisted int a, @Named("cat2") String b) {}

    @AssistedInject
    Cat(@Assisted byte a, @Named("catfail") String b) {} // not a dependency!

    @Inject
    void register(@Named("cat3") String a) {}
  }

  public void testFactoryPublicAndReturnTypeNotPublic() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(
                  new FactoryModuleBuilder()
                      .implement(Hidden.class, HiddenImpl.class)
                      .build(NotHidden.class));
            }
          });
      fail("Expected CreationException");
    } catch (CreationException ce) {
      assertEquals(
          NotHidden.class.getName()
              + " is public, but has a method that returns a non-public type: "
              + Hidden.class.getName()
              + ". Due to limitations with java.lang.reflect.Proxy, this is not allowed. "
              + "Please either make the factory non-public or the return type public.",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }

  interface Hidden {}

  public static class HiddenImpl implements Hidden {}

  public interface NotHidden {
    Hidden create();
  }

  public void testSingletonScopeOnAssistedClassIsIgnored() {
    try {
      Guice.createInjector(
          new AbstractModule() {
            @Override
            protected void configure() {
              install(new FactoryModuleBuilder().build(SingletonFactory.class));
            }
          });
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertEquals(
          "Found scope annotation ["
              + Singleton.class.getName()
              + "]"
              + " on implementation class ["
              + AssistedSingleton.class.getName()
              + "]"
              + " of AssistedInject factory ["
              + SingletonFactory.class.getName()
              + "]."
              + "\nThis is not allowed, please remove the scope annotation.",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }

  interface SingletonFactory {
    AssistedSingleton create(String string);
  }

  @SuppressWarnings("GuiceAssistedInjectScoping")
  @Singleton
  static class AssistedSingleton {
    @Inject
    public AssistedSingleton(@SuppressWarnings("unused") @Assisted String string) {}
  }
}
