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

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;

/**
 * @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() {
          protected void configure() {
            addError(new Exception("A"));
          }
        },

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

  public void testErrorsAddedWhenExceptionsAreThrown() {
    checkModule(
        new AbstractModule() {
          protected void configure() {
            install(new AbstractModule() {
              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() {
          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() {
          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() {
          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() {
          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() {
          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>() {
      public String get() {
        return "A";
      }
    };
    
    final javax.inject.Provider<Integer> intJavaxProvider = new javax.inject.Provider<Integer>() {
      public Integer get() {
        return 42;
      }
    };
    
    final javax.inject.Provider<Double> doubleJavaxProvider = new javax.inject.Provider<Double>() {
      @javax.inject.Inject String string;
      
      public Double get() {
        return 42.42;
      }
    };

    checkModule(
        new AbstractModule() {
          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() {
          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() {
          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() {
          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() {
              public Void visitEagerSingleton() {
                return null;
              }
            });
            return null;
          }
        }
    );
  }

  public void testBindToInstanceInScope() {
    checkModule(
        new AbstractModule() {
          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() {
          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() {
              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() {
      public Object invoke(org.aopalliance.intercept.MethodInvocation methodInvocation) {
        return null;
      }
    };

    checkModule(
        new AbstractModule() {
          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() {
          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() {
      public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
        throw new UnsupportedOperationException();
      }
    };
    
    checkModule(
        new AbstractModule() {
          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() {
      public Object convert(String value, TypeLiteral<?> toType) {
        return value;
      }
    };

    checkModule(
        new AbstractModule() {
          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() {
          protected void configure() {
            Provider<String> keyGetProvider
                = getProvider(Key.get(String.class, SampleAnnotation.class));
            try {
              keyGetProvider.get();
            } 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();
            } 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<String>();
    final MembersInjector<String> stringInjector = new MembersInjector<String>() {
      public void injectMembers(String instance) {
        lastInjected.set(instance);
      }
    };

    checkModule(
        new AbstractModule() {
          protected void configure() {
            providerFromBinder.set(getProvider(String.class));
            membersInjectorFromBinder.set(getMembersInjector(String.class));
          }
        },

        new FailingElementVisitor() {
          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() {
          protected void configure() {
            MembersInjector<A<String>> typeMembersInjector
                = getMembersInjector(new TypeLiteral<A<String>>() {});
            try {
              typeMembersInjector.injectMembers(new A<String>());
            } 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");
            } 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() {
          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() {
          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() {
          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() {
          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() {
          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() {
          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() {
          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() {
          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() {
          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() {
          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() {
      public void configure() {
        aConfigureCount.incrementAndGet();
      }
    };

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

    aConfigureCount.set(0);
    Module b = new AbstractModule() {
      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> {
    public List get() {
      return new ArrayList();
    }
  }

  private static class TProvider<T> implements Provider<T> {
    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 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) {}
  }
}
