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

import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.inject.Asserts.assertContains;
import static com.google.inject.Asserts.getDeclaringSourcePart;
import static com.google.inject.Asserts.isIncludeStackTraceComplete;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.BindingAnnotation;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.MembersInjector;
import com.google.inject.Module;
import com.google.inject.PrivateBinder;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.binder.AnnotatedConstantBindingBuilder;
import com.google.inject.binder.ConstantBindingBuilder;
import com.google.inject.binder.ScopedBindingBuilder;
import com.google.inject.matcher.Matcher;
import com.google.inject.matcher.Matchers;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.google.inject.util.Providers;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import junit.framework.TestCase;

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

  // Binder fidelity tests

  public void testAddMessageErrorCommand() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            addError("Message %s %d %s", "A", 5, "C");
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals("Message A 5 C", command.getMessage());
            assertNull(command.getCause());
            assertContains(
                command.getSources().toString(),
                ElementsTest.class.getName(),
                getDeclaringSourcePart(ElementsTest.class));
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testAddThrowableErrorCommand() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            addError(new Exception("A"));
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals("A", command.getCause().getMessage());
            assertEquals("An exception was caught and reported. Message: A", command.getMessage());
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testErrorsAddedWhenExceptionsAreThrown() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            install(
                new AbstractModule() {
                  @Override
                  protected void configure() {
                    throw new RuntimeException(
                        "Throwing RuntimeException in AbstractModule.configure().");
                  }
                });

            addError("Code after the exception still gets executed");
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals(
                "Throwing RuntimeException in AbstractModule.configure().",
                command.getCause().getMessage());
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals("Code after the exception still gets executed", command.getMessage());
            return null;
          }
        });
  }

  private <T> T getInstance(Binding<T> binding) {
    return binding.acceptTargetVisitor(Elements.<T>getInstanceVisitor());
  }

  public void testBindConstantAnnotations() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bindConstant().annotatedWith(SampleAnnotation.class).to("A");
            bindConstant().annotatedWith(Names.named("Bee")).to("B");
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey());
            assertEquals("A", getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(String.class, Names.named("Bee")), command.getKey());
            assertEquals("B", getInstance(command));
            return null;
          }
        });
  }

  public void testBindConstantTypes() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bindConstant().annotatedWith(Names.named("String")).to("A");
            bindConstant().annotatedWith(Names.named("int")).to(2);
            bindConstant().annotatedWith(Names.named("long")).to(3L);
            bindConstant().annotatedWith(Names.named("boolean")).to(false);
            bindConstant().annotatedWith(Names.named("double")).to(5.0d);
            bindConstant().annotatedWith(Names.named("float")).to(6.0f);
            bindConstant().annotatedWith(Names.named("short")).to((short) 7);
            bindConstant().annotatedWith(Names.named("char")).to('h');
            bindConstant().annotatedWith(Names.named("byte")).to((byte) 8);
            bindConstant().annotatedWith(Names.named("Class")).to(Iterator.class);
            bindConstant().annotatedWith(Names.named("Enum")).to(CoinSide.TAILS);
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(String.class, Names.named("String")), command.getKey());
            assertEquals("A", getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Integer.class, Names.named("int")), command.getKey());
            assertEquals(2, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Long.class, Names.named("long")), command.getKey());
            assertEquals(3L, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Boolean.class, Names.named("boolean")), command.getKey());
            assertEquals(false, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Double.class, Names.named("double")), command.getKey());
            assertEquals(5.0d, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Float.class, Names.named("float")), command.getKey());
            assertEquals(6.0f, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Short.class, Names.named("short")), command.getKey());
            assertEquals((short) 7, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Character.class, Names.named("char")), command.getKey());
            assertEquals('h', getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Byte.class, Names.named("byte")), command.getKey());
            assertEquals((byte) 8, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(Class.class, Names.named("Class")), command.getKey());
            assertEquals(Iterator.class, getInstance(command));
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(CoinSide.class, Names.named("Enum")), command.getKey());
            assertEquals(CoinSide.TAILS, getInstance(command));
            return null;
          }
        });
  }

  public void testBindKeysNoAnnotations() {
    FailingElementVisitor keyChecker =
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(String.class), command.getKey());
            return null;
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class).toInstance("A");
            bind(new TypeLiteral<String>() {}).toInstance("B");
            bind(Key.get(String.class)).toInstance("C");
          }
        },
        keyChecker,
        keyChecker,
        keyChecker);
  }

  public void testBindKeysWithAnnotationType() {
    FailingElementVisitor annotationChecker =
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey());
            return null;
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class).annotatedWith(SampleAnnotation.class).toInstance("A");
            bind(new TypeLiteral<String>() {})
                .annotatedWith(SampleAnnotation.class)
                .toInstance("B");
          }
        },
        annotationChecker,
        annotationChecker);
  }

  public void testBindKeysWithAnnotationInstance() {
    FailingElementVisitor annotationChecker =
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(String.class, Names.named("a")), command.getKey());
            return null;
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class).annotatedWith(Names.named("a")).toInstance("B");
            bind(new TypeLiteral<String>() {}).annotatedWith(Names.named("a")).toInstance("C");
          }
        },
        annotationChecker,
        annotationChecker);
  }

  public void testBindToProvider() {
    final Provider<String> aProvider =
        new Provider<String>() {
          @Override
          public String get() {
            return "A";
          }
        };

    final javax.inject.Provider<Integer> intJavaxProvider =
        new javax.inject.Provider<Integer>() {
          @Override
          public Integer get() {
            return 42;
          }
        };

    final javax.inject.Provider<Double> doubleJavaxProvider =
        new javax.inject.Provider<Double>() {
          @javax.inject.Inject String string;

          @Override
          public Double get() {
            return 42.42;
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class).toProvider(aProvider);
            bind(Integer.class).toProvider(intJavaxProvider);
            bind(Double.class).toProvider(doubleJavaxProvider);
            bind(List.class).toProvider(ListProvider.class);
            bind(Collection.class).toProvider(Key.get(ListProvider.class));
            bind(Iterable.class).toProvider(new TypeLiteral<TProvider<List>>() {});
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof ProviderInstanceBinding);
            assertEquals(Key.get(String.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ProviderInstanceBinding<? extends T> binding) {
                    assertSame(aProvider, binding.getUserSuppliedProvider());
                    assertSame(aProvider, binding.getProviderInstance());
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof ProviderInstanceBinding);
            assertEquals(Key.get(Integer.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ProviderInstanceBinding<? extends T> binding) {
                    assertSame(intJavaxProvider, binding.getUserSuppliedProvider());
                    assertEquals(42, binding.getProviderInstance().get());
                    // we don't wrap this w/ dependencies if there were none.
                    assertFalse(binding.getProviderInstance() instanceof HasDependencies);
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof ProviderInstanceBinding);
            assertEquals(Key.get(Double.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ProviderInstanceBinding<? extends T> binding) {
                    assertSame(doubleJavaxProvider, binding.getUserSuppliedProvider());
                    assertEquals(42.42, binding.getProviderInstance().get());
                    // we do wrap it with dependencies if there were some.
                    assertTrue(binding.getProviderInstance() instanceof HasDependencies);
                    Set<Dependency<?>> deps =
                        ((HasDependencies) binding.getProviderInstance()).getDependencies();
                    assertEquals(1, deps.size());
                    assertEquals(
                        String.class,
                        deps.iterator().next().getKey().getTypeLiteral().getRawType());
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof ProviderKeyBinding);
            assertEquals(Key.get(List.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ProviderKeyBinding<? extends T> binding) {
                    assertEquals(Key.get(ListProvider.class), binding.getProviderKey());
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof ProviderKeyBinding);
            assertEquals(Key.get(Collection.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ProviderKeyBinding<? extends T> binding) {
                    assertEquals(Key.get(ListProvider.class), binding.getProviderKey());
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof ProviderKeyBinding);
            assertEquals(Key.get(Iterable.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ProviderKeyBinding<? extends T> binding) {
                    assertEquals(new Key<TProvider<List>>() {}, binding.getProviderKey());
                    return null;
                  }
                });
            return null;
          }
        });
  }

  public void testBindToLinkedBinding() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(List.class).to(ArrayList.class);
            bind(Map.class).to(new TypeLiteral<HashMap<Integer, String>>() {});
            bind(Set.class).to(Key.get(TreeSet.class, SampleAnnotation.class));
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof LinkedKeyBinding);
            assertEquals(Key.get(List.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(LinkedKeyBinding<? extends T> binding) {
                    assertEquals(Key.get(ArrayList.class), binding.getLinkedKey());
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof LinkedKeyBinding);
            assertEquals(Key.get(Map.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(LinkedKeyBinding<? extends T> binding) {
                    assertEquals(
                        Key.get(new TypeLiteral<HashMap<Integer, String>>() {}),
                        binding.getLinkedKey());
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof LinkedKeyBinding);
            assertEquals(Key.get(Set.class), command.getKey());
            command.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(LinkedKeyBinding<? extends T> binding) {
                    assertEquals(
                        Key.get(TreeSet.class, SampleAnnotation.class), binding.getLinkedKey());
                    return null;
                  }
                });
            return null;
          }
        });
  }

  public void testBindToInstance() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class).toInstance("A");
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertTrue(command instanceof InstanceBinding);
            assertEquals(Key.get(String.class), command.getKey());
            assertEquals("A", getInstance(command));
            return null;
          }
        });
  }

  public void testBindInScopes() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class);
            bind(List.class).to(ArrayList.class).in(Scopes.SINGLETON);
            bind(Map.class).to(HashMap.class).in(Singleton.class);
            bind(Set.class).to(TreeSet.class).asEagerSingleton();
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(String.class), command.getKey());
            command.acceptScopingVisitor(
                new FailingBindingScopingVisitor() {
                  @Override
                  public Void visitNoScoping() {
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(List.class), command.getKey());
            command.acceptScopingVisitor(
                new FailingBindingScopingVisitor() {
                  @Override
                  public Void visitScope(Scope scope) {
                    assertEquals(Scopes.SINGLETON, scope);
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(Map.class), command.getKey());
            command.acceptScopingVisitor(
                new FailingBindingScopingVisitor() {
                  @Override
                  public Void visitScopeAnnotation(Class<? extends Annotation> annotation) {
                    assertEquals(Singleton.class, annotation);
                    return null;
                  }
                });
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            assertEquals(Key.get(Set.class), command.getKey());
            command.acceptScopingVisitor(
                new FailingBindingScopingVisitor() {
                  @Override
                  public Void visitEagerSingleton() {
                    return null;
                  }
                });
            return null;
          }
        });
  }

  public void testBindToInstanceInScope() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            AnnotatedBindingBuilder<String> b = bind(String.class);
            b.toInstance("A");
            b.in(Singleton.class);
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals(
                "Setting the scope is not permitted when binding to a single instance.",
                command.getMessage());
            assertNull(command.getCause());
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testBindToInstanceScope() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(String.class).toInstance("A");
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> binding) {
            assertEquals(Key.get(String.class), binding.getKey());
            binding.acceptScopingVisitor(
                new FailingBindingScopingVisitor() {
                  @Override
                  public Void visitEagerSingleton() {
                    return null;
                  }
                });
            return null;
          }
        });
  }

  /*if[AOP]*/
  public void testBindIntercepor() {
    final Matcher<Class> classMatcher = Matchers.subclassesOf(List.class);
    final Matcher<Object> methodMatcher = Matchers.any();
    final org.aopalliance.intercept.MethodInterceptor methodInterceptor =
        new org.aopalliance.intercept.MethodInterceptor() {
          @Override
          public Object invoke(org.aopalliance.intercept.MethodInvocation methodInvocation) {
            return null;
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bindInterceptor(classMatcher, methodMatcher, methodInterceptor);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(InterceptorBinding command) {
            assertSame(classMatcher, command.getClassMatcher());
            assertSame(methodMatcher, command.getMethodMatcher());
            assertEquals(Arrays.asList(methodInterceptor), command.getInterceptors());
            return null;
          }
        });
  }
  /*end[AOP]*/

  public void testBindScope() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bindScope(SampleAnnotation.class, Scopes.NO_SCOPE);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(ScopeBinding command) {
            assertSame(SampleAnnotation.class, command.getAnnotationType());
            assertSame(Scopes.NO_SCOPE, command.getScope());
            return null;
          }
        });
  }

  public void testBindListener() {
    final Matcher<Object> typeMatcher = Matchers.only(TypeLiteral.get(String.class));
    final TypeListener listener =
        new TypeListener() {
          @Override
          public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
            throw new UnsupportedOperationException();
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bindListener(typeMatcher, listener);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(TypeListenerBinding binding) {
            assertSame(typeMatcher, binding.getTypeMatcher());
            assertSame(listener, binding.getListener());
            return null;
          }
        });
  }

  public void testConvertToTypes() {
    final TypeConverter typeConverter =
        new TypeConverter() {
          @Override
          public Object convert(String value, TypeLiteral<?> toType) {
            return value;
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            convertToTypes(Matchers.any(), typeConverter);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(TypeConverterBinding command) {
            assertSame(typeConverter, command.getTypeConverter());
            assertSame(Matchers.any(), command.getTypeMatcher());
            return null;
          }
        });
  }

  public void testGetProvider() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            Provider<String> keyGetProvider =
                getProvider(Key.get(String.class, SampleAnnotation.class));
            try {
              keyGetProvider.get();
              fail("Expected IllegalStateException");
            } catch (IllegalStateException e) {
              assertEquals(
                  "This Provider cannot be used until the Injector has been created.",
                  e.getMessage());
            }

            Provider<String> typeGetProvider = getProvider(String.class);
            try {
              typeGetProvider.get();
              fail("Expected IllegalStateException");
            } catch (IllegalStateException e) {
              assertEquals(
                  "This Provider cannot be used until the Injector has been created.",
                  e.getMessage());
            }
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(ProviderLookup<T> command) {
            assertEquals(Key.get(String.class, SampleAnnotation.class), command.getKey());
            assertNull(command.getDelegate());
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(ProviderLookup<T> command) {
            assertEquals(Key.get(String.class), command.getKey());
            assertNull(command.getDelegate());
            return null;
          }
        });
  }

  public void testElementInitialization() {
    final AtomicReference<Provider<String>> providerFromBinder =
        new AtomicReference<Provider<String>>();
    final AtomicReference<MembersInjector<String>> membersInjectorFromBinder =
        new AtomicReference<MembersInjector<String>>();

    final AtomicReference<String> lastInjected = new AtomicReference<>();
    final MembersInjector<String> stringInjector =
        new MembersInjector<String>() {
          @Override
          public void injectMembers(String instance) {
            lastInjected.set(instance);
          }
        };

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            providerFromBinder.set(getProvider(String.class));
            membersInjectorFromBinder.set(getMembersInjector(String.class));
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(ProviderLookup<T> providerLookup) {
            @SuppressWarnings("unchecked") // we know that T is a String here
            ProviderLookup<String> stringLookup = (ProviderLookup<String>) providerLookup;
            stringLookup.initializeDelegate(Providers.of("out"));

            assertEquals("out", providerFromBinder.get().get());
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(MembersInjectorLookup<T> lookup) {
            @SuppressWarnings("unchecked") // we know that T is a String here
            MembersInjectorLookup<String> stringLookup = (MembersInjectorLookup<String>) lookup;
            stringLookup.initializeDelegate(stringInjector);

            membersInjectorFromBinder.get().injectMembers("in");
            assertEquals("in", lastInjected.get());
            return null;
          }
        });
  }

  public void testGetMembersInjector() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            MembersInjector<A<String>> typeMembersInjector =
                getMembersInjector(new TypeLiteral<A<String>>() {});
            try {
              typeMembersInjector.injectMembers(new A<String>());
              fail("Expected IllegalStateException");
            } catch (IllegalStateException e) {
              assertEquals(
                  "This MembersInjector cannot be used until the Injector has been created.",
                  e.getMessage());
            }

            MembersInjector<String> classMembersInjector = getMembersInjector(String.class);
            try {
              classMembersInjector.injectMembers("hello");
              fail("Expected IllegalStateException");
            } catch (IllegalStateException e) {
              assertEquals(
                  "This MembersInjector cannot be used until the Injector has been created.",
                  e.getMessage());
            }
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(MembersInjectorLookup<T> command) {
            assertEquals(new TypeLiteral<A<String>>() {}, command.getType());
            assertNull(command.getDelegate());
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(MembersInjectorLookup<T> command) {
            assertEquals(TypeLiteral.get(String.class), command.getType());
            assertNull(command.getDelegate());
            return null;
          }
        });
  }

  public void testRequestInjection() {
    final Object firstObject = new Object();
    final Object secondObject = new Object();

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            requestInjection(firstObject);
            requestInjection(secondObject);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(InjectionRequest<?> command) {
            assertEquals(firstObject, command.getInstance());
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(InjectionRequest<?> command) {
            assertEquals(secondObject, command.getInstance());
            return null;
          }
        });
  }

  public void testRequestStaticInjection() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            requestStaticInjection(ArrayList.class);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(StaticInjectionRequest command) {
            assertEquals(ArrayList.class, command.getType());
            return null;
          }
        });
  }

  public void testNewPrivateBinder() {
    final Key<Collection> collection = Key.get(Collection.class, SampleAnnotation.class);
    final Key<ArrayList> arrayList = Key.get(ArrayList.class);
    final ImmutableSet<Key<?>> collections = ImmutableSet.<Key<?>>of(arrayList, collection);

    final Key<?> a = Key.get(String.class, Names.named("a"));
    final Key<?> b = Key.get(String.class, Names.named("b"));
    final ImmutableSet<Key<?>> ab = ImmutableSet.of(a, b);

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            PrivateBinder one = binder().newPrivateBinder();
            one.expose(ArrayList.class);
            one.expose(Collection.class).annotatedWith(SampleAnnotation.class);
            one.bind(List.class).to(ArrayList.class);

            PrivateBinder two =
                binder().withSource("1 FooBar").newPrivateBinder().withSource("2 FooBar");
            two.expose(String.class).annotatedWith(Names.named("a"));
            two.expose(b);
            two.bind(List.class).to(ArrayList.class);
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(PrivateElements one) {
            assertEquals(collections, one.getExposedKeys());
            checkElements(
                one.getElements(),
                new FailingElementVisitor() {
                  @Override
                  public <T> Void visit(Binding<T> binding) {
                    assertEquals(Key.get(List.class), binding.getKey());
                    return null;
                  }
                });
            return null;
          }
        },
        new ExternalFailureVisitor() {
          @Override
          public Void visit(PrivateElements two) {
            assertEquals(ab, two.getExposedKeys());
            assertEquals("1 FooBar", two.getSource().toString());
            checkElements(
                two.getElements(),
                new ExternalFailureVisitor() {
                  @Override
                  public <T> Void visit(Binding<T> binding) {
                    assertEquals("2 FooBar", binding.getSource().toString());
                    assertEquals(Key.get(List.class), binding.getKey());
                    return null;
                  }
                });
            return null;
          }
        });
  }

  public void testBindWithMultipleAnnotationsAddsError() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            AnnotatedBindingBuilder<String> abb = bind(String.class);
            abb.annotatedWith(SampleAnnotation.class);
            abb.annotatedWith(Names.named("A"));
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals(
                "More than one annotation is specified for this binding.", command.getMessage());
            assertNull(command.getCause());
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testBindWithMultipleTargetsAddsError() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            AnnotatedBindingBuilder<String> abb = bind(String.class);
            abb.toInstance("A");
            abb.toInstance("B");
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals("Implementation is set more than once.", command.getMessage());
            assertNull(command.getCause());
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testBindWithMultipleScopesAddsError() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            ScopedBindingBuilder sbb = bind(List.class).to(ArrayList.class);
            sbb.in(Scopes.NO_SCOPE);
            sbb.asEagerSingleton();
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals("Scope is set more than once.", command.getMessage());
            assertNull(command.getCause());
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testBindConstantWithMultipleAnnotationsAddsError() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            AnnotatedConstantBindingBuilder cbb = bindConstant();
            cbb.annotatedWith(SampleAnnotation.class).to("A");
            cbb.annotatedWith(Names.named("A"));
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message command) {
            assertEquals(
                "More than one annotation is specified for this binding.", command.getMessage());
            assertNull(command.getCause());
            assertContains(command.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testBindConstantWithMultipleTargetsAddsError() {
    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            ConstantBindingBuilder cbb = bindConstant().annotatedWith(SampleAnnotation.class);
            cbb.to("A");
            cbb.to("B");
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> command) {
            return null;
          }
        },
        new FailingElementVisitor() {
          @Override
          public Void visit(Message message) {
            assertEquals("Constant value is set more than once.", message.getMessage());
            assertNull(message.getCause());
            assertContains(message.getSource(), getDeclaringSourcePart(ElementsTest.class));
            return null;
          }
        });
  }

  public void testBindToConstructor() throws NoSuchMethodException, NoSuchFieldException {
    final Constructor<A> aConstructor = A.class.getDeclaredConstructor();
    final Constructor<B> bConstructor = B.class.getDeclaredConstructor(Object.class);
    final Field field = B.class.getDeclaredField("stage");

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(A.class).toConstructor(aConstructor);
            bind(B.class)
                .toConstructor(bConstructor, new TypeLiteral<B<Integer>>() {})
                .in(Singleton.class);
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> binding) {
            assertEquals(new Key<A>() {}, binding.getKey());

            return binding.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ConstructorBinding<? extends T> constructorBinding) {
                    InjectionPoint injectionPoint = constructorBinding.getConstructor();
                    assertEquals(aConstructor, injectionPoint.getMember());
                    assertEquals(new TypeLiteral<A>() {}, injectionPoint.getDeclaringType());
                    return null;
                  }
                });
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> binding) {
            assertEquals(new Key<B>() {}, binding.getKey());
            binding.acceptScopingVisitor(
                new FailingBindingScopingVisitor() {
                  @Override
                  public Void visitScopeAnnotation(Class<? extends Annotation> annotation) {
                    assertEquals(Singleton.class, annotation);
                    return null;
                  }
                });

            binding.acceptTargetVisitor(
                new FailingTargetVisitor<T>() {
                  @Override
                  public Void visit(ConstructorBinding<? extends T> constructorBinding) {
                    assertEquals(bConstructor, constructorBinding.getConstructor().getMember());
                    assertEquals(
                        Key.get(Integer.class),
                        getOnlyElement(constructorBinding.getConstructor().getDependencies())
                            .getKey());
                    assertEquals(
                        field,
                        getOnlyElement(constructorBinding.getInjectableMembers()).getMember());
                    assertEquals(2, constructorBinding.getDependencies().size());
                    /*if[AOP]*/
                    assertEquals(ImmutableMap.of(), constructorBinding.getMethodInterceptors());
                    /*end[AOP]*/
                    return null;
                  }
                });
            return null;
          }
        });
  }

  public void testBindToMalformedConstructor() throws NoSuchMethodException, NoSuchFieldException {
    final Constructor<C> constructor = C.class.getDeclaredConstructor(Integer.class);

    checkModule(
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(C.class).toConstructor(constructor);
          }
        },
        new FailingElementVisitor() {
          @Override
          public <T> Void visit(Binding<T> binding) {
            assertEquals(Key.get(C.class), binding.getKey());
            assertTrue(binding instanceof UntargettedBinding);
            return null;
          }
        },
        new ExternalFailureVisitor() {
          @Override
          public Void visit(Message message) {
            assertContains(
                message.getMessage(),
                C.class.getName() + ".a has more than one annotation ",
                Named.class.getName(),
                SampleAnnotation.class.getName());
            return null;
          }
        },
        new ExternalFailureVisitor() {
          @Override
          public Void visit(Message message) {
            assertContains(
                message.getMessage(),
                C.class.getName() + ".<init>() has more than one annotation ",
                Named.class.getName(),
                SampleAnnotation.class.getName());
            return null;
          }
        });
  }

  // Business logic tests

  public void testModulesAreInstalledAtMostOnce() {
    final AtomicInteger aConfigureCount = new AtomicInteger(0);
    final Module a =
        new AbstractModule() {
          @Override
          public void configure() {
            aConfigureCount.incrementAndGet();
          }
        };

    Elements.getElements(a, a);
    assertEquals(1, aConfigureCount.get());

    aConfigureCount.set(0);
    Module b =
        new AbstractModule() {
          @Override
          protected void configure() {
            install(a);
            install(a);
          }
        };

    Elements.getElements(b);
    assertEquals(1, aConfigureCount.get());
  }

  /** Ensures the module performs the commands consistent with {@code visitors}. */
  protected void checkModule(Module module, ElementVisitor<?>... visitors) {
    List<Element> elements = Elements.getElements(module);
    assertEquals(elements.size(), visitors.length);
    checkElements(elements, visitors);
  }

  protected void checkElements(List<Element> elements, ElementVisitor<?>... visitors) {
    for (int i = 0; i < visitors.length; i++) {
      ElementVisitor<?> visitor = visitors[i];
      Element element = elements.get(i);
      if (!(element instanceof Message)) {
        ElementSource source = (ElementSource) element.getSource();
        assertFalse(source.getModuleClassNames().isEmpty());
        if (isIncludeStackTraceComplete()) {
          assertTrue(source.getStackTrace().length > 0);
        } else {
          assertEquals(0, source.getStackTrace().length);
        }
      }
      if (!(visitor instanceof ExternalFailureVisitor)) {
        assertContains(element.getSource().toString(), getDeclaringSourcePart(ElementsTest.class));
      }
      element.acceptVisitor(visitor);
    }
  }

  private static class ListProvider implements Provider<List> {
    @Override
    public List get() {
      return new ArrayList();
    }
  }

  private static class TProvider<T> implements Provider<T> {
    @Override
    public T get() {
      return null;
    }
  }

  /**
   * By extending this interface rather than FailingElementVisitor, the source of the error doesn't
   * need to contain the string {@code ElementsTest.java}.
   */
  abstract static class ExternalFailureVisitor extends FailingElementVisitor {}

  @Retention(RUNTIME)
  @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
  @BindingAnnotation
  public @interface SampleAnnotation {}

  public enum CoinSide {
    HEADS,
    TAILS
  }

  static class A<T> {
    @Inject Stage stage;
  }

  static class B<T> {
    @Inject Stage stage;

    B(T t) {}
  }

  static class C {
    @Inject
    @Named("foo")
    @SampleAnnotation
    String a;

    C(@Named("bar") @SampleAnnotation Integer b) {}
  }
}
