/**
 * 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.assistedinject;

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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Stage;
import com.google.inject.name.Named;
import com.google.inject.spi.DefaultBindingTargetVisitor;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

/**
 * Tests for AssistedInject Spi.
 *
 * @author ramakrishna@google.com (Ramakrishna Rajanna)
 */
public class ExtensionSpiTest extends TestCase {

  public final void testSpiOnElements() throws Exception {
    AssistedInjectSpiVisitor visitor = new AssistedInjectSpiVisitor();
    Integer count = 0;
    for(Element element : Elements.getElements(new Module())) {
      if(element instanceof Binding) {
        assertEquals(count++, ((Binding<?>)element).acceptTargetVisitor(visitor));
      }
    }
    validateVisitor(visitor);
  }

  public void testSpiOnVisitor() throws Exception {
    AssistedInjectSpiVisitor visitor = new AssistedInjectSpiVisitor();
    Integer count = 0;
    Injector injector = Guice.createInjector(new Module());
    for(Binding<?> binding : injector.getBindings().values()) {
      assertEquals(count++, binding.acceptTargetVisitor(visitor));
    }
    validateVisitor(visitor);
  }

  private void validateVisitor(AssistedInjectSpiVisitor visitor) throws Exception {
    assertEquals(1, visitor.assistedBindingCount);
    List<AssistedMethod> assistedMethods =
        Lists.newArrayList(Iterables.getOnlyElement(
            visitor.assistedInjectBindings).getAssistedMethods());
    assertEquals(7, assistedMethods.size());
    assertEquals(1, visitor.assistedBindingCount);
    assertEquals(1, visitor.assistedInjectBindings.size());

    // Validate for each of the methods in AnimalFactory
    
    Set<String> names = Sets.newHashSet();
    for (AssistedMethod method : assistedMethods) {
      String name = method.getFactoryMethod().getName();
      names.add(name);
      if (name.equals("createAStrangeCatAsAnimal")) {        
        validateAssistedMethod(method, name, StrangeCat.class, ImmutableList.<Key<?>>of());        
      } else if (name.equals("createStrangeCatWithConstructorForOwner")) {
        validateAssistedMethod(method, name, StrangeCat.class, ImmutableList.<Key<?>>of());     
      } else if (name.equals("createStrangeCatWithConstructorForAge")) {
        validateAssistedMethod(method, name, StrangeCat.class, ImmutableList.<Key<?>>of());        
      } else if (name.equals("createCatWithANonAssistedDependency")) {
        validateAssistedMethod(method, name, CatWithAName.class,
            ImmutableList.<Key<?>>of(Key.get(String.class, named("catName2"))));        
      } else if (name.equals("createCat")) {
        validateAssistedMethod(method, name, Cat.class, ImmutableList.<Key<?>>of());        
      } else if (name.equals("createASimpleCatAsAnimal")) {
        validateAssistedMethod(method, name, SimpleCat.class, ImmutableList.<Key<?>>of());
      } else if (name.equals("createCatWithNonAssistedDependencies")) {
        List<Key<?>> dependencyKeys = ImmutableList.<Key<?>>of(
            Key.get(String.class, named("catName1")),
            Key.get(String.class, named("petName")),        
            Key.get(Integer.class, named("age")));
        validateAssistedMethod(method, name, ExplodingCat.class, dependencyKeys);        
      } else {
        fail("Invalid method: " + method);
      }
    }
    assertEquals(names, ImmutableSet.of("createAStrangeCatAsAnimal",
        "createStrangeCatWithConstructorForOwner",
        "createStrangeCatWithConstructorForAge",
        "createCatWithANonAssistedDependency",
        "createCat",
        "createASimpleCatAsAnimal",
        "createCatWithNonAssistedDependencies"));
  }

  private void validateAssistedMethod(AssistedMethod assistedMethod, String factoryMethodName,
                                      Class clazz, List<Key<?>> dependencyKeys){
    assertEquals(factoryMethodName, assistedMethod.getFactoryMethod().getName());
    assertEquals(clazz, assistedMethod.getImplementationConstructor().getDeclaringClass());
    assertEquals(dependencyKeys.size(), assistedMethod.getDependencies().size());
    for (Dependency<?> dependency : assistedMethod.getDependencies()) {
      assertTrue(dependencyKeys.contains(dependency.getKey()));
    }
    assertEquals(clazz, assistedMethod.getImplementationType().getType());
  }


  interface AnimalFactory {
    Cat createCat(String owner);
    CatWithAName createCatWithANonAssistedDependency(String owner);
    @Named("SimpleCat") Animal createASimpleCatAsAnimal(String owner);
    Animal createAStrangeCatAsAnimal(String owner);
    StrangeCat createStrangeCatWithConstructorForOwner(String owner);
    StrangeCat createStrangeCatWithConstructorForAge(Integer age);
    ExplodingCat createCatWithNonAssistedDependencies(String owner);
  }

  interface Animal {}

  private static class Cat implements Animal {
    @Inject Cat(@Assisted String owner) {}
  }

  private static class SimpleCat implements Animal {
    @Inject SimpleCat(@Assisted String owner) {
    }
  }

  private static class StrangeCat implements Animal {
    @AssistedInject StrangeCat(@Assisted String owner) {}
    @AssistedInject StrangeCat(@Assisted Integer age) {}
  }

  private static class ExplodingCat implements Animal {
    @Inject public ExplodingCat(@Named("catName1") String name, @Assisted String owner,
                                @Named("age") Integer age, @Named("petName") String petName) {}
  }

  private static class CatWithAName extends Cat {
    @Inject CatWithAName(@Assisted String owner, @Named("catName2") String name) {
      super(owner);
    }
  }

  public class Module extends AbstractModule{
    @Override
    protected void configure() {
      bind(String.class).annotatedWith(named("catName1")).toInstance("kitty1");
      bind(String.class).annotatedWith(named("catName2")).toInstance("kitty2");
      bind(String.class).annotatedWith(named("petName")).toInstance("pussy");
      bind(Integer.class).annotatedWith(named("age")).toInstance(12);
      install(new FactoryModuleBuilder()
        .implement(Animal.class, StrangeCat.class)
        .implement(Animal.class, named("SimpleCat"), SimpleCat.class)
        .build(AnimalFactory.class));
    }
  }

  public class AssistedInjectSpiVisitor extends DefaultBindingTargetVisitor<Object, Integer>
      implements AssistedInjectTargetVisitor<Object, Integer>  {

    private final Set<Class> allowedClasses = 
      ImmutableSet.<Class> of(
        Injector.class, Stage.class, Logger.class,
        String.class, Integer.class);
    
    private int assistedBindingCount = 0;
    private int currentCount = 0;
    private List<AssistedInjectBinding<?>> assistedInjectBindings = Lists.newArrayList();

    public Integer visit(AssistedInjectBinding assistedInjectBinding) {
      assistedInjectBindings.add(assistedInjectBinding);
      assistedBindingCount++;
      return currentCount++;
    }

    @Override
    protected Integer visitOther(Binding<? extends Object> binding) {
      if(!allowedClasses.contains(binding.getKey().getTypeLiteral().getRawType())) {
        throw new AssertionFailedError("invalid other binding: " + binding);
      }
      return currentCount++;
    }
  }
}
