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

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Asserts;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Scope;
import com.google.inject.ScopeAnnotation;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.util.Classes;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.Message;
import com.google.inject.throwingproviders.ThrowingProviderBinder.Result;

import junit.framework.TestCase;

import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.BindException;
import java.rmi.AccessException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TooManyListenersException;

/**
 * @author jmourits@google.com (Jerome Mourits)
 * @author jessewilson@google.com (Jesse Wilson)
 * @author sameb@google.com (Sam Berlin)
 */
public class CheckedProviderTest extends TestCase {
  
  private static final Function<Dependency<?>, Key<?>> DEPENDENCY_TO_KEY =
      new Function<Dependency<?>, Key<?>>() {
        public Key<?> apply(Dependency<?> from) {
          return from.getKey();
        }
      };

  private final TypeLiteral<RemoteProvider<Foo>> remoteProviderOfFoo
      = new TypeLiteral<RemoteProvider<Foo>>() { };
  private final MockRemoteProvider<Foo> mockRemoteProvider = new MockRemoteProvider<Foo>();
  private final TestScope testScope = new TestScope();
  
  private Injector bindInjector;  
  private Injector providesInjector;
  private Injector cxtorInjector;
  
  @Override
  protected void setUp() throws Exception {
    MockFoo.nextToThrow = null;
    MockFoo.nextToReturn = null;
    AnotherMockFoo.nextToThrow = null;
    AnotherMockFoo.nextToReturn = null;
    
    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .to(mockRemoteProvider)
            .in(testScope);
      }
    });  
    
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
       install(ThrowingProviderBinder.forModule(this));
       bindScope(TestScope.Scoped.class, testScope);
      }
      
      @SuppressWarnings("unused")
      @CheckedProvides(RemoteProvider.class)
      @TestScope.Scoped
      Foo throwOrGet() throws RemoteException, BindException {
        return mockRemoteProvider.get();
      }
    });
    
    cxtorInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
          .bind(RemoteProvider.class, Foo.class)
          .providing(MockFoo.class)
          .in(testScope);
      }
    });
  }

  public void testExceptionsThrown_Bind() throws Exception {
    tExceptionsThrown(bindInjector);
  }
  
  public void testExceptionsThrown_Provides() throws Exception {
    tExceptionsThrown(providesInjector);
  }
  
  public void testExceptionsThrown_Cxtor() throws Exception {
    tExceptionsThrown(cxtorInjector);
  }
  
  private void tExceptionsThrown(Injector injector) throws Exception {
    RemoteProvider<Foo> remoteProvider = 
      injector.getInstance(Key.get(remoteProviderOfFoo));

    mockRemoteProvider.throwOnNextGet(new BindException("kaboom!"));
    MockFoo.nextToThrow = new BindException("kaboom!");
    try {
      remoteProvider.get();
      fail();
    } catch (BindException expected) {
      assertEquals("kaboom!", expected.getMessage());
    }
  }

  public void testValuesScoped_Bind() throws Exception  {
    tValuesScoped(bindInjector);
  }
  
  public void testValuesScoped_Provides() throws Exception  {
    tValuesScoped(providesInjector);
  }
  
  private void tValuesScoped(Injector injector) throws Exception {
    RemoteProvider<Foo> remoteProvider = 
      injector.getInstance(Key.get(remoteProviderOfFoo));

    mockRemoteProvider.setNextToReturn(new SimpleFoo("A"));
    assertEquals("A", remoteProvider.get().s());

    mockRemoteProvider.setNextToReturn(new SimpleFoo("B"));
    assertEquals("A", remoteProvider.get().s());

    testScope.beginNewScope();
    assertEquals("B", remoteProvider.get().s());
  }
  
  public void testValuesScoped_Cxtor() throws Exception {
    RemoteProvider<Foo> remoteProvider = 
        cxtorInjector.getInstance(Key.get(remoteProviderOfFoo));

    Foo retrieved = remoteProvider.get();
    assertSame(retrieved, remoteProvider.get()); // same, not in new scope.
    
    testScope.beginNewScope();
    assertNotSame(retrieved, remoteProvider.get()); // different, new scope.
  }

  public void testExceptionsScoped_Bind() throws Exception {
    tExceptionsScoped(bindInjector);
  }
  
  public void testExceptionsScoped_Provides() throws Exception {
    tExceptionsScoped(providesInjector);
  }
  
  public void testExceptionScopes_Cxtor() throws Exception {
    tExceptionsScoped(cxtorInjector);
  }
  
  private void tExceptionsScoped(Injector injector) throws Exception {
    RemoteProvider<Foo> remoteProvider = 
        injector.getInstance(Key.get(remoteProviderOfFoo));

    mockRemoteProvider.throwOnNextGet(new RemoteException("A"));
    MockFoo.nextToThrow = new RemoteException("A");
    try {
      remoteProvider.get();
      fail();
    } catch (RemoteException expected) {
      assertEquals("A", expected.getMessage());
    }
    
    mockRemoteProvider.throwOnNextGet(new RemoteException("B"));
    MockFoo.nextToThrow = new RemoteException("B");
    try {
      remoteProvider.get();
      fail();
    } catch (RemoteException expected) {
      assertEquals("A", expected.getMessage());
    }
  }
  
  public void testAnnotations_Bind() throws Exception {
    final MockRemoteProvider<Foo> mockRemoteProviderA = new MockRemoteProvider<Foo>();
    final MockRemoteProvider<Foo> mockRemoteProviderB = new MockRemoteProvider<Foo>();
    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .annotatedWith(Names.named("a"))
            .to(mockRemoteProviderA);

        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .to(mockRemoteProviderB);
      }
    });
    tAnnotations(bindInjector, mockRemoteProviderA, mockRemoteProviderB);
  }
  
  public void testAnnotations_Provides() throws Exception {
    final MockRemoteProvider<Foo> mockRemoteProviderA = new MockRemoteProvider<Foo>();
    final MockRemoteProvider<Foo> mockRemoteProviderB = new MockRemoteProvider<Foo>();
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        install(ThrowingProviderBinder.forModule(this));
       }
       
       @SuppressWarnings("unused")
       @CheckedProvides(RemoteProvider.class)
       @Named("a")
       Foo throwOrGet() throws RemoteException, BindException {
         return mockRemoteProviderA.get();
       }
       
       @SuppressWarnings("unused")
       @CheckedProvides(RemoteProvider.class)
       Foo throwOrGet2() throws RemoteException, BindException {
         return mockRemoteProviderB.get();
       }
    });
    tAnnotations(providesInjector, mockRemoteProviderA, mockRemoteProviderB);
  }
  
  private void tAnnotations(Injector injector, MockRemoteProvider<Foo> mockA,
      MockRemoteProvider<Foo> mockB) throws Exception {
    mockA.setNextToReturn(new SimpleFoo("A"));
    mockB.setNextToReturn(new SimpleFoo("B"));
    assertEquals("A", 
        injector.getInstance(Key.get(remoteProviderOfFoo, Names.named("a"))).get().s());

    assertEquals("B", 
        injector.getInstance(Key.get(remoteProviderOfFoo)).get().s());
  }
  
  public void testAnnotations_Cxtor() throws Exception {
    cxtorInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .annotatedWith(Names.named("a"))
            .providing(MockFoo.class);

        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .providing(AnotherMockFoo.class);
      }
    });
    MockFoo.nextToReturn = "A";
    AnotherMockFoo.nextToReturn = "B";
    assertEquals("A", 
        cxtorInjector.getInstance(Key.get(remoteProviderOfFoo, Names.named("a"))).get().s());

    assertEquals("B", 
        cxtorInjector.getInstance(Key.get(remoteProviderOfFoo)).get().s());
  }
  
  public void testUndeclaredExceptions_Bind() throws Exception {
    tUndeclaredExceptions(bindInjector);
  }
  
  public void testUndeclaredExceptions_Provides() throws Exception {
    tUndeclaredExceptions(providesInjector);
  }
  
  public void testUndeclaredExceptions_Cxtor() throws Exception {
    tUndeclaredExceptions(cxtorInjector);
  }

  private void tUndeclaredExceptions(Injector injector) throws Exception { 
    RemoteProvider<Foo> remoteProvider = 
        injector.getInstance(Key.get(remoteProviderOfFoo));
    mockRemoteProvider.throwOnNextGet(new IndexOutOfBoundsException("A"));
    MockFoo.nextToThrow = new IndexOutOfBoundsException("A");
    try {
      remoteProvider.get();
      fail();
    } catch (RuntimeException e) {
      assertEquals("A", e.getCause().getMessage());
    }

    // undeclared exceptions shouldn't be scoped
    mockRemoteProvider.throwOnNextGet(new IndexOutOfBoundsException("B"));
    MockFoo.nextToThrow = new IndexOutOfBoundsException("B");
    try {
      remoteProvider.get();
      fail();
    } catch (RuntimeException e) {
      assertEquals("B", e.getCause().getMessage());
    }
  }

  public void testThrowingProviderSubclassing() throws Exception {
    final SubMockRemoteProvider aProvider = new SubMockRemoteProvider();
    aProvider.setNextToReturn(new SimpleFoo("A"));

    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .to(aProvider);
      }
    });

    assertEquals("A",
        bindInjector.getInstance(Key.get(remoteProviderOfFoo)).get().s());
  }

  static class SubMockRemoteProvider extends MockRemoteProvider<Foo> { }

  public void testBindingToNonInterfaceType_Bind() throws Exception {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(MockRemoteProvider.class, Foo.class)
              .to(mockRemoteProvider);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertEquals(MockRemoteProvider.class.getName() + " must be an interface",
          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
    }
  }
  
  public void testBindingToNonInterfaceType_Provides() throws Exception {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
          
        @SuppressWarnings("unused")
        @CheckedProvides(MockRemoteProvider.class)
        Foo foo() {
          return null;
        }
      });
      fail();
    } catch (CreationException expected) {
      assertEquals(MockRemoteProvider.class.getName() + " must be an interface",
          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
    }
  }  
  
  public void testBindingToSubSubInterface_Bind() throws Exception {
    try {
      bindInjector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(SubRemoteProvider.class, Foo.class);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertEquals(SubRemoteProvider.class.getName() + " must extend CheckedProvider (and only CheckedProvider)",
          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
    }
  }
  
  public void testBindingToSubSubInterface_Provides() throws Exception {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
          
        @SuppressWarnings("unused")
        @CheckedProvides(SubRemoteProvider.class)
        Foo foo() {
          return null;
        }
      });
      fail();
    } catch (CreationException expected) {
      assertEquals(SubRemoteProvider.class.getName() + " must extend CheckedProvider (and only CheckedProvider)",
          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
    }
  }    

  interface SubRemoteProvider extends RemoteProvider<String> { }

  public void testBindingToInterfaceWithExtraMethod_Bind() throws Exception {
    try {
      bindInjector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProviderWithExtraMethod.class, Foo.class);
        }
      });
      fail();
    } catch (CreationException expected) {
      assertEquals(RemoteProviderWithExtraMethod.class.getName() + " may not declare any new methods, but declared " 
          + RemoteProviderWithExtraMethod.class.getDeclaredMethods()[0].toGenericString(),
          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
    }
  }
  
  public void testBindingToInterfaceWithExtraMethod_Provides() throws Exception {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
          
        @SuppressWarnings("unused")
        @CheckedProvides(RemoteProviderWithExtraMethod.class)
        Foo foo() {
          return null;
        }
      });
      fail();
    } catch (CreationException expected) {
      assertEquals(RemoteProviderWithExtraMethod.class.getName() + " may not declare any new methods, but declared " 
          + RemoteProviderWithExtraMethod.class.getDeclaredMethods()[0].toGenericString(),
          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
    }
  }
  
  public void testDependencies_Bind() {
    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(String.class).toInstance("Foo");
        bind(Integer.class).toInstance(5);
        bind(Double.class).toInstance(5d);
        bind(Long.class).toInstance(5L);
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .to(DependentRemoteProvider.class);
      }
    });
    
    HasDependencies hasDependencies =
        (HasDependencies)bindInjector.getBinding(Key.get(remoteProviderOfFoo));
    hasDependencies = 
        (HasDependencies)bindInjector.getBinding(
            Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey());
    // Make sure that that is dependent on DependentRemoteProvider.
    assertEquals(Dependency.get(Key.get(DependentRemoteProvider.class)), 
        Iterables.getOnlyElement(hasDependencies.getDependencies()));
    // And make sure DependentRemoteProvider has the proper dependencies.
    hasDependencies = (HasDependencies)bindInjector.getBinding(DependentRemoteProvider.class);
    Set<Key<?>> dependencyKeys = ImmutableSet.copyOf(
        Iterables.transform(hasDependencies.getDependencies(), DEPENDENCY_TO_KEY));
    assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class), Key.get(Integer.class),
        Key.get(Long.class), Key.get(Double.class)), dependencyKeys);
  }
  
  public void testDependencies_Provides() {
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(String.class).toInstance("Foo");
        bind(Integer.class).toInstance(5);
        bind(Double.class).toInstance(5d);
        bind(Long.class).toInstance(5L);
        install(ThrowingProviderBinder.forModule(this));
      }
      
      @SuppressWarnings("unused")
      @CheckedProvides(RemoteProvider.class)
      Foo foo(String s, Integer i, Double d, Long l) {
        return null;
      }
    });
    
    HasDependencies hasDependencies =
        (HasDependencies) providesInjector.getBinding(Key.get(remoteProviderOfFoo));
    // RemoteProvider<String> is dependent on the provider method..
    hasDependencies = (HasDependencies) providesInjector.getBinding(
        Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey());
    // And the provider method has our real dependencies..
    hasDependencies = (HasDependencies)providesInjector.getBinding(
        Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey());
    Set<Key<?>> dependencyKeys = ImmutableSet.copyOf(
        Iterables.transform(hasDependencies.getDependencies(), DEPENDENCY_TO_KEY));
    assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class), Key.get(Integer.class),
        Key.get(Long.class), Key.get(Double.class)), dependencyKeys);
  }  
  
  public void testDependencies_Cxtor() {
    cxtorInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(String.class).toInstance("Foo");
        bind(Integer.class).toInstance(5);
        bind(Double.class).toInstance(5d);
        bind(Long.class).toInstance(5L);
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .providing(DependentMockFoo.class);
      }
    });
    
    Key<?> key = Key.get(remoteProviderOfFoo);
    
    // RemoteProvider<String> is dependent on Result.
    HasDependencies hasDependencies = (HasDependencies) cxtorInjector.getBinding(key);
    key = Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey();
    assertEquals(Result.class, key.getTypeLiteral().getRawType());

    // Result is dependent on the fake CheckedProvider impl
    hasDependencies = (HasDependencies) cxtorInjector.getBinding(key);
    key = Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey();
    assertTrue(CheckedProvider.class.isAssignableFrom(key.getTypeLiteral().getRawType()));
    
    // And the CheckedProvider is dependent on DependentMockFoo...
    hasDependencies = (HasDependencies) cxtorInjector.getBinding(key);
    key = Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey();
    assertEquals(DependentMockFoo.class, key.getTypeLiteral().getRawType());
    
    // And DependentMockFoo is dependent on the goods.
    hasDependencies = (HasDependencies) cxtorInjector.getBinding(key); 
    Set<Key<?>> dependencyKeys = ImmutableSet.copyOf(
        Iterables.transform(hasDependencies.getDependencies(), DEPENDENCY_TO_KEY));
    assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class), Key.get(Integer.class),
        Key.get(Long.class), Key.get(Double.class)), dependencyKeys);
  }  

  interface RemoteProviderWithExtraMethod<T> extends CheckedProvider<T> {
    T get(T defaultValue) throws RemoteException, BindException;
  }

  interface RemoteProvider<T> extends CheckedProvider<T> { 
    public T get() throws RemoteException, BindException;
  }
  
  static class DependentMockFoo implements Foo {
    @Inject double foo;
    
    @ThrowingInject public DependentMockFoo(String foo, int bar) {
    }
    
    @Inject void initialize(long foo) {}
    
    @Override
    public String s() {
      return null;
    }
  }
  
  static class DependentRemoteProvider<T> implements RemoteProvider<T> {
    @Inject double foo;
    
    @Inject public DependentRemoteProvider(String foo, int bar) {
    }
    
    @Inject void initialize(long foo) {}
    
    public T get() {
      return null;
    }
  }
  
  interface Foo {
    String s();
  }
  
  static class SimpleFoo implements Foo {
    private String s;
    
    SimpleFoo(String s) {
      this.s = s;
    }
    
    @Override
    public String s() {
      return s;
    }
    
    @Override
    public String toString() {
      return s;
    }
  }
  
  static class MockFoo implements Foo {
    static Exception nextToThrow;    
    static String nextToReturn;
    
    @ThrowingInject
    MockFoo() throws RemoteException, BindException {
      if (nextToThrow instanceof RemoteException) {
        throw (RemoteException) nextToThrow;
      } else if (nextToThrow instanceof BindException) {
        throw (BindException) nextToThrow;
      } else if (nextToThrow instanceof RuntimeException) {
        throw (RuntimeException) nextToThrow;
      } else if (nextToThrow == null) {
        // Do nothing, return this.
      } else {
        throw new AssertionError("nextToThrow must be a runtime or remote exception");
      }
    }
    
    @Override
    public String s() {
      return nextToReturn;
    }
    
    @Override
    public String toString() {
      return nextToReturn;
    }
  }
  
  static class AnotherMockFoo implements Foo {
    static Exception nextToThrow;    
    static String nextToReturn;
    
    @ThrowingInject
    AnotherMockFoo() throws RemoteException, BindException {
      if (nextToThrow instanceof RemoteException) {
        throw (RemoteException) nextToThrow;
      } else if (nextToThrow instanceof BindException) {
        throw (BindException) nextToThrow;
      } else if (nextToThrow instanceof RuntimeException) {
        throw (RuntimeException) nextToThrow;
      } else if (nextToThrow == null) {
        // Do nothing, return this.
      } else {
        throw new AssertionError("nextToThrow must be a runtime or remote exception");
      }
    }
    
    @Override
    public String s() {
      return nextToReturn;
    }
    
    @Override
    public String toString() {
      return nextToReturn;
    }
  }
  
  static class MockRemoteProvider<T> implements RemoteProvider<T> {
    Exception nextToThrow;
    T nextToReturn;

    public void throwOnNextGet(Exception nextToThrow) {
      this.nextToThrow = nextToThrow;
    }

    public void setNextToReturn(T nextToReturn) {
      this.nextToReturn = nextToReturn;
    }
    
    public T get() throws RemoteException, BindException {
      if (nextToThrow instanceof RemoteException) {
        throw (RemoteException) nextToThrow;
      } else if (nextToThrow instanceof BindException) {
        throw (BindException) nextToThrow;
      } else if (nextToThrow instanceof RuntimeException) {
        throw (RuntimeException) nextToThrow;
      } else if (nextToThrow == null) {
        return nextToReturn;
      } else {
        throw new AssertionError("nextToThrow must be a runtime or remote exception");
      }
    }
  }

  public void testBindingToInterfaceWithBoundValueType_Bind() throws RemoteException {
    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(StringRemoteProvider.class, String.class)
            .to(new StringRemoteProvider() {
              public String get() {
                return "A";
              }
            });
      }
    });
    
    assertEquals("A", bindInjector.getInstance(StringRemoteProvider.class).get());
  }
  
  public void testBindingToInterfaceWithBoundValueType_Provides() throws RemoteException {
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        install(ThrowingProviderBinder.forModule(this));
      }
      
      @SuppressWarnings("unused")
      @CheckedProvides(StringRemoteProvider.class)
      String foo() throws RemoteException {
          return "A";
      }
    });
    
    assertEquals("A", providesInjector.getInstance(StringRemoteProvider.class).get());
  }

  interface StringRemoteProvider extends CheckedProvider<String> {
    @Override String get() throws RemoteException;  
  }

  @SuppressWarnings("deprecation")
  public void testBindingToInterfaceWithGeneric_Bind() throws Exception {
    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, new TypeLiteral<List<String>>() { }.getType())
            .to(new RemoteProvider<List<String>>() {
              public List<String> get() {
                return Arrays.asList("A", "B");
              }
            });
      }
    });

    Key<RemoteProvider<List<String>>> key
        = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
    assertEquals(Arrays.asList("A", "B"), bindInjector.getInstance(key).get());
  }
  
  public void testBindingToInterfaceWithGeneric_BindUsingTypeLiteral() throws Exception {
    bindInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, new TypeLiteral<List<String>>() {})
            .to(new RemoteProvider<List<String>>() {
              public List<String> get() {
                return Arrays.asList("A", "B");
              }
            });
      }
    });

    Key<RemoteProvider<List<String>>> key
        = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
    assertEquals(Arrays.asList("A", "B"), bindInjector.getInstance(key).get());
  }
  
  public void testBindingToInterfaceWithGeneric_Provides() throws Exception {
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        install(ThrowingProviderBinder.forModule(this));
      }
      
      @SuppressWarnings("unused")
      @CheckedProvides(RemoteProvider.class)
      List<String> foo() throws RemoteException {
          return Arrays.asList("A", "B");
      }
    });

    Key<RemoteProvider<List<String>>> key
        = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
    assertEquals(Arrays.asList("A", "B"), providesInjector.getInstance(key).get());
  }
  
  public void testBindingToInterfaceWithGeneric_Cxtor() throws Exception {
    cxtorInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
        .bind(RemoteProvider.class, new TypeLiteral<List<String>>() {})
        .providing(new TypeLiteral<ThrowingArrayList<String>>() {});
      }
    });

    Key<RemoteProvider<List<String>>> key
        = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
    assertEquals(Arrays.asList(), cxtorInjector.getInstance(key).get());
  }
  
  private static class ThrowingArrayList<T> extends ArrayList<T> {
    @SuppressWarnings("unused")
    @ThrowingInject
    ThrowingArrayList() {}
  }
  
  public void testProviderMethodWithWrongException() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(RemoteProvider.class)
        String foo() throws InterruptedException {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(InterruptedException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  public void testCxtorWithWrongException() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(WrongExceptionFoo.class);
        }
      });
      fail();
    } catch (CreationException ce) {
      assertEquals(InterruptedException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  static class WrongExceptionFoo implements Foo {
    @SuppressWarnings("unused")
    @ThrowingInject
    public WrongExceptionFoo() throws InterruptedException {
    }
    
    @Override
    public String s() { return null; }
  }
  
  public void testProviderMethodWithSubclassOfExceptionIsOk() throws Exception {
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        install(ThrowingProviderBinder.forModule(this));
      }
      
      @SuppressWarnings("unused")
      @CheckedProvides(RemoteProvider.class)
      Foo foo() throws AccessException {
        throw new AccessException("boo!");
      }
    });
    
    RemoteProvider<Foo> remoteProvider = 
      providesInjector.getInstance(Key.get(remoteProviderOfFoo));

    try {
      remoteProvider.get();
      fail();
    } catch (RemoteException expected) {
      assertTrue(expected instanceof AccessException);
      assertEquals("boo!", expected.getMessage());
    }
  }
  
  public void testCxtorWithSubclassOfExceptionIsOk() throws Exception {
    cxtorInjector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(SubclassExceptionFoo.class);
        }
      });
    
    RemoteProvider<Foo> remoteProvider = 
        cxtorInjector.getInstance(Key.get(remoteProviderOfFoo));

      try {
        remoteProvider.get();
        fail();
      } catch (RemoteException expected) {
        assertTrue(expected instanceof AccessException);
        assertEquals("boo!", expected.getMessage());
      }
  }
  
  static class SubclassExceptionFoo implements Foo {
    @ThrowingInject
    public SubclassExceptionFoo() throws AccessException {
      throw new AccessException("boo!");
    }
    
    @Override
    public String s() { return null; }
  }
  
  public void testProviderMethodWithSuperclassExceptionFails() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(RemoteProvider.class)
        Foo foo() throws IOException {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(IOException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  public void testCxtorWithSuperclassExceptionFails() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(SuperclassExceptionFoo.class);
        }
      });
      fail();
    } catch (CreationException ce) {
      assertEquals(IOException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  static class SuperclassExceptionFoo implements Foo {
    @SuppressWarnings("unused")
    @ThrowingInject
    public SuperclassExceptionFoo() throws IOException {
    }
    
    @Override
    public String s() { return null; }
  }
  
  public void testProviderMethodWithRuntimeExceptionsIsOk() throws Exception {
    providesInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        install(ThrowingProviderBinder.forModule(this));
      }
      
      @SuppressWarnings("unused")
      @CheckedProvides(RemoteProvider.class)
      Foo foo() throws RuntimeException {
        throw new RuntimeException("boo!");
      }
    });
    
    RemoteProvider<Foo> remoteProvider = 
      providesInjector.getInstance(Key.get(remoteProviderOfFoo));

    try {
      remoteProvider.get();
      fail();
    } catch (RuntimeException expected) {
      assertEquals("boo!", expected.getCause().getMessage());
    }
  }
  
  public void testCxtorWithRuntimeExceptionsIsOk() throws Exception {
    cxtorInjector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        ThrowingProviderBinder.create(binder())
            .bind(RemoteProvider.class, Foo.class)
            .providing(RuntimeExceptionFoo.class);
      }
    });
    
    RemoteProvider<Foo> remoteProvider = 
        cxtorInjector.getInstance(Key.get(remoteProviderOfFoo));

    try {
      remoteProvider.get();
      fail();
    } catch (RuntimeException expected) {
      assertEquals("boo!", expected.getCause().getMessage());
    }
  }
    
  static class RuntimeExceptionFoo implements Foo {
    @ThrowingInject
    public RuntimeExceptionFoo() throws RuntimeException {
      throw new RuntimeException("boo!");
    }
    
    @Override
    public String s() { return null; }
  }
  
  private static class SubBindException extends BindException {}
  
  public void testProviderMethodWithManyExceptions() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(RemoteProvider.class)
        String foo() throws InterruptedException, RuntimeException, RemoteException, 
                            AccessException, TooManyListenersException,
                            BindException, SubBindException {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      // The only two that should fail are Interrupted & TooManyListeners.. the rest are OK.
      List<Message> errors = ImmutableList.copyOf(ce.getErrorMessages());
      assertEquals(InterruptedException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          errors.get(0).getMessage());
      assertEquals(TooManyListenersException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          errors.get(1).getMessage());
      assertEquals(2, errors.size());
    }
  }
  
  public void testCxtorWithManyExceptions() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(ManyExceptionFoo.class);
        }
      });
      fail();
    } catch (CreationException ce) {
      // The only two that should fail are Interrupted & TooManyListeners.. the rest are OK.
      List<Message> errors = ImmutableList.copyOf(ce.getErrorMessages());
      assertEquals(InterruptedException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          errors.get(0).getMessage());
      assertEquals(TooManyListenersException.class.getName()
          + " is not compatible with the exceptions (["
          + RemoteException.class + ", " + BindException.class
          + "]) declared in the CheckedProvider interface ("
          + RemoteProvider.class.getName()
          + ")", 
          errors.get(1).getMessage());
      assertEquals(2, errors.size());
    }
  }
  
  static class ManyExceptionFoo implements Foo {
    @SuppressWarnings("unused")
    @ThrowingInject
    public ManyExceptionFoo()
        throws InterruptedException,
        RuntimeException,
        RemoteException,
        AccessException,
        TooManyListenersException,
        BindException,
        SubBindException {
    }
    
    @Override
    public String s() { return null; }
  }
  
  public void testMoreTypeParameters() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(TooManyTypeParameters.class)
        String foo() {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(TooManyTypeParameters.class.getName() + " has more than one generic type parameter: [T, P]",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }    
  }
  
  public void testWrongThrowingProviderType() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(WrongThrowingProviderType.class)
        String foo() {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(WrongThrowingProviderType.class.getName() 
          + " does not properly extend CheckedProvider, the first type parameter of CheckedProvider "
          + "(java.lang.String) is not a generic type",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }    
  }
  
  public void testOneMethodThatIsntGet() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(OneNoneGetMethod.class)
        String foo() {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(OneNoneGetMethod.class.getName() 
          + " may not declare any new methods, but declared " + Classes.toString(OneNoneGetMethod.class.getDeclaredMethods()[0]),
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  public void testManyMethods() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(ManyMethods.class)
        String foo() {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(ManyMethods.class.getName() 
          + " may not declare any new methods, but declared " + Arrays.asList(ManyMethods.class.getDeclaredMethods()),
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  public void testIncorrectPredefinedType_Bind() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(StringRemoteProvider.class, Integer.class)
              .to(new StringRemoteProvider() {
                public String get() {
                  return "A";
                }
              });
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(StringRemoteProvider.class.getName() 
          + " expects the value type to be java.lang.String, but it was java.lang.Integer",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  public void testIncorrectPredefinedType_Provides() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          install(ThrowingProviderBinder.forModule(this));
        }
        
        @SuppressWarnings("unused")
        @CheckedProvides(StringRemoteProvider.class)
        Integer foo() {
            return null;
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals(StringRemoteProvider.class.getName() 
          + " expects the value type to be java.lang.String, but it was java.lang.Integer",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  private static interface TooManyTypeParameters<T, P> extends CheckedProvider<T> {    
  }
  
  private static interface WrongThrowingProviderType<T> extends CheckedProvider<String> {    
  }
  
  private static interface OneNoneGetMethod<T> extends CheckedProvider<T> {
    T bar();
  }
  
  private static interface ManyMethods<T> extends CheckedProvider<T> {
    T bar();
    String baz();
  }
  
  public void testResultSerializes() throws Exception {
    Result result = Result.forValue("foo");
    result = Asserts.reserialize(result);
    assertEquals("foo", result.getOrThrow());
  }
  
  public void testResultExceptionSerializes() throws Exception {
    Result result = Result.forException(new Exception("boo"));
    result = Asserts.reserialize(result);
    try {
      result.getOrThrow();
      fail();
    } catch(Exception ex) {
      assertEquals("boo", ex.getMessage());
    }
  }
  
  public void testEarlyBindingError() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(StringRemoteProvider.class, String.class)
              .to(FailingProvider.class);
        }
      });
      fail();
    } catch(CreationException ce) {
      assertEquals("Could not find a suitable constructor in " + FailingProvider.class.getName()
          + ". Classes must have either one (and only one) constructor annotated with @Inject"
          + " or a zero-argument constructor that is not private.",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  private static class FailingProvider implements StringRemoteProvider {
    // no @Inject.
    @SuppressWarnings("unused")
    FailingProvider(Integer foo) {}
    
    public String get() {
      return null;
    }
  }
  
  public void testNoInjectionPointForUsing() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(InvalidFoo.class);
        }
      });
      fail();
    } catch (CreationException ce) {
      assertEquals("Could not find a suitable constructor in " + InvalidFoo.class.getName()
          + ". Classes must have either one (and only one) constructor annotated with "
          + "@ThrowingInject.",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  static class InvalidFoo implements Foo {
    public InvalidFoo(String dep) {
    }
    
    @Override public String s() { return null; }
  }
  
  public void testNoThrowingInject() {
    try {
      Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(NormalInjectableFoo.class);
        }
      });
      fail();
    } catch (CreationException ce) {
      assertEquals("Could not find a suitable constructor in " + NormalInjectableFoo.class.getName()
          + ". Classes must have either one (and only one) constructor annotated with "
          + "@ThrowingInject.",
          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
    }
  }
  
  static class NormalInjectableFoo implements Foo {
    @Inject
    public NormalInjectableFoo() {
    }
    
    @Override public String s() { return null; }
  }
  
  public void testProvisionExceptionOnDependenciesOfCxtor() throws Exception {
    Injector injector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(ProvisionExceptionFoo.class);
          bindScope(BadScope.class, new Scope() {
            @Override
            public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
              return new Provider<T>() {
                @Override
                public T get() {
                  throw new OutOfScopeException("failure");
                }
              };
            }
          });
        }
      });
    
    try {
      injector.getInstance(Key.get(remoteProviderOfFoo)).get();
      fail();
    } catch(ProvisionException pe) {
      assertEquals(2, pe.getErrorMessages().size());
      List<Message> messages = Lists.newArrayList(pe.getErrorMessages());
      assertEquals("Error in custom provider, com.google.inject.OutOfScopeException: failure",
          messages.get(0).getMessage());
      assertEquals("Error in custom provider, com.google.inject.OutOfScopeException: failure",
          messages.get(1).getMessage());
    }
  }
  
  @ScopeAnnotation
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  private @interface BadScope { }
  
  @BadScope private static class Unscoped1 {}
  @BadScope private static class Unscoped2 {}
  
  static class ProvisionExceptionFoo implements Foo {
    @ThrowingInject
    public ProvisionExceptionFoo(Unscoped1 a, Unscoped2 b) {
    }
    
    @Override public String s() { return null; }
  }
  
  public void testUsingDoesntClashWithBindingsOfSameType() throws Exception {
    cxtorInjector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          ThrowingProviderBinder.create(binder())
              .bind(RemoteProvider.class, Foo.class)
              .providing(MockFoo.class);
          bind(Foo.class).to(MockFoo.class);
          bind(MockFoo.class).to(SubMockFoo.class);
        }
      });
    
    RemoteProvider<Foo> remoteProvider = 
        cxtorInjector.getInstance(Key.get(remoteProviderOfFoo));
    Foo providerGot = remoteProvider.get();
    Foo fooGot = cxtorInjector.getInstance(Foo.class);
    Foo mockGot = cxtorInjector.getInstance(MockFoo.class);
    
    assertEquals(MockFoo.class, providerGot.getClass());
    assertEquals(SubMockFoo.class, fooGot.getClass());
    assertEquals(SubMockFoo.class, mockGot.getClass());
  }
  
  static class SubMockFoo extends MockFoo {
    public SubMockFoo() throws RemoteException, BindException {
    }
    
  }
}
