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

import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.google.inject.spi.ConstructorBinding;
import com.google.inject.spi.ConvertedConstantBinding;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.InstanceBinding;
import com.google.inject.spi.LinkedKeyBinding;
import com.google.inject.spi.ProviderBinding;
import com.google.inject.spi.ProviderInstanceBinding;
import com.google.inject.spi.ProviderKeyBinding;
import java.util.Collection;
import java.util.Set;
import junit.framework.TestCase;

/**
 * Tests for {@link TransitiveDependencyVisitor}.
 *
 * @author phopkins@gmail.com (Pete Hopkins)
 */
public class TransitiveDependencyVisitorTest extends TestCase {
  private TransitiveDependencyVisitor visitor;

  @Override
  protected void setUp() throws Exception {
    super.setUp();

    visitor = new TransitiveDependencyVisitor();
  }

  public void testVisitConstructor() {
    Binding<?> binding = getBinding(Key.get(ConstructedClass.class));
    Collection<Key<?>> dependencies = visitor.visit((ConstructorBinding<?>) binding);

    assertDependencies(
        dependencies, Key.get(A.class), Key.get(B.class), Key.get(C.class), Key.get(D.class));
  }

  public void testVisitConvertedConstant() {
    Binding<?> binding =
        getBinding(Key.get(Integer.class, Names.named("number")), new ConvertedConstantModule());
    Collection<Key<?>> dependencies = visitor.visit((ConvertedConstantBinding<?>) binding);

    assertDependencies(dependencies, Key.get(String.class, Names.named("number")));
  }

  public void testVisitInstance() {
    Binding<?> binding = getBinding(Key.get(ConstructedClass.class), new InstanceModule());
    Collection<Key<?>> dependencies = visitor.visit((InstanceBinding<?>) binding);

    // Dependencies will only be on the field- and method-injected classes.
    assertDependencies(dependencies, Key.get(A.class), Key.get(D.class));
  }

  public void testVisitInstance_instanceHasDependencies() {
    Binding<?> binding = getBinding(Key.get(Interface.class), new HasDependenciesModule());
    Collection<Key<?>> dependencies = visitor.visit((InstanceBinding<?>) binding);

    // Dependencies should only be on the stated
    // HasDependencies#getDependencies() values
    assertDependencies(dependencies, Key.get(G.class));
  }

  public void testVisitLinkedKey() {
    Binding<?> binding = getBinding(Key.get(Interface.class), new LinkedKeyModule());
    Collection<Key<?>> dependencies = visitor.visit((LinkedKeyBinding<?>) binding);

    // Dependency should be to the class this interface is bound to.
    assertDependencies(dependencies, Key.get(ConstructedClass.class));
  }

  public void testVisitProviderBinding() {
    Binding<?> binding = getBinding(Key.get(new TypeLiteral<Provider<ConstructedClass>>() {}));
    Collection<Key<?>> dependencies = visitor.visit((ProviderBinding<?>) binding);

    assertDependencies(dependencies, Key.get(ConstructedClass.class));
  }

  public void testVisitProviderInstance() {
    Binding<?> binding = getBinding(Key.get(ConstructedClass.class), new ProviderInstanceModule());
    Collection<Key<?>> dependencies = visitor.visit((ProviderInstanceBinding<?>) binding);

    // Dependencies will only be on the field- and method-injected classes.
    assertDependencies(dependencies, Key.get(E.class), Key.get(F.class));
  }

  public void testVisitProviderKey() {
    Binding<?> binding = getBinding(Key.get(ConstructedClass.class), new ProviderKeyModule());
    Collection<Key<?>> dependencies = visitor.visit((ProviderKeyBinding<?>) binding);

    // Dependency should be to the class that provides this one.
    assertDependencies(dependencies, Key.get(ConstructedClassProvider.class));
  }

  private Binding<?> getBinding(Key<?> key, Module... modules) {
    return Guice.createInjector(modules).getBinding(key);
  }

  private void assertDependencies(Collection<Key<?>> dependencies, Key<?>... keys) {
    assertNotNull("Dependencies should not be null", dependencies);
    assertEquals(
        "There should be " + keys.length + " dependencies", keys.length, dependencies.size());

    for (Key<?> key : keys) {
      assertTrue("Dependencies should contain " + key, dependencies.contains(key));
    }
  }

  private static class A {}

  private static class B {}

  private static class C {}

  private static class D {}

  private static class E {}

  private static class F {}

  private static class G {}

  private static interface Interface {}

  private static class ConstructedClass implements Interface {
    @Inject A a;

    ConstructedClass() {}

    @Inject
    ConstructedClass(B b, C c) {}

    @Inject
    void setD(D d) {}
  }

  private static class ConstructedClassProvider implements Provider<ConstructedClass> {
    @Inject E e;

    ConstructedClassProvider() {}

    @Inject
    ConstructedClassProvider(A a, B b, C c) {}

    @Inject
    void setF(F f) {}

    @Override
    public ConstructedClass get() {
      return null;
    }
  }

  private static class HasDependenciesClass implements Interface, HasDependencies {
    @Inject A a;
    @Inject B b;

    @Override
    public Set<Dependency<?>> getDependencies() {
      return ImmutableSet.<Dependency<?>>of(Dependency.get(Key.get(G.class)));
    }
  }

  private static class ConvertedConstantModule extends AbstractModule {
    @Override
    protected void configure() {
      bindConstant().annotatedWith(Names.named("number")).to("2008");
    }
  }

  private static class InstanceModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(ConstructedClass.class).toInstance(new ConstructedClass());
    }
  }

  private static class LinkedKeyModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(Interface.class).to(ConstructedClass.class);
    }
  }

  private static class ProviderInstanceModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(ConstructedClass.class).toProvider(new ConstructedClassProvider());
    }
  }

  private static class HasDependenciesModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(Interface.class).toInstance(new HasDependenciesClass());
    }
  }

  private static class ProviderKeyModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(ConstructedClass.class).toProvider(ConstructedClassProvider.class);
    }
  }
}
