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

import static com.google.inject.Asserts.assertContains;
import static com.google.inject.Asserts.getDeclaringSourcePart;
import static com.google.inject.Asserts.reserialize;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import junit.framework.TestCase;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * @author jessewilson@google.com (Jesse Wilson)
 */
@SuppressWarnings("UnusedDeclaration")
public class ProvisionExceptionTest extends TestCase {

  public void testExceptionsCollapsed() {
    try {
      Guice.createInjector().getInstance(A.class);
      fail(); 
    } catch (ProvisionException e) {
      assertTrue(e.getCause() instanceof UnsupportedOperationException);
      assertContains(e.getMessage(), "Error injecting constructor",
          "for parameter 0 at com.google.inject.ProvisionExceptionTest$C.setD",
          "for field at com.google.inject.ProvisionExceptionTest$B.c",
          "for parameter 0 at com.google.inject.ProvisionExceptionTest$A");
    }
  }

  /**
   * There's a pass-through of user code in the scope. We want exceptions thrown by Guice to be
   * limited to a single exception, even if it passes through user code.
   */
  public void testExceptionsCollapsedWithScopes() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(B.class).in(Scopes.SINGLETON);
        }
      }).getInstance(A.class);
      fail();
    } catch (ProvisionException e) {
      assertTrue(e.getCause() instanceof UnsupportedOperationException);
      assertFalse(e.getMessage().contains("custom provider"));
      assertContains(e.getMessage(), "Error injecting constructor",
          "for parameter 0 at com.google.inject.ProvisionExceptionTest$C.setD",
          "for field at com.google.inject.ProvisionExceptionTest$B.c",
          "for parameter 0 at com.google.inject.ProvisionExceptionTest$A");
    }
  }

  public void testMethodInjectionExceptions() {
    try {
      Guice.createInjector().getInstance(E.class);
      fail();
    } catch (ProvisionException e) {
      assertTrue(e.getCause() instanceof UnsupportedOperationException);
      assertContains(e.getMessage(), "Error injecting method",
          "at " + E.class.getName() + ".setObject(ProvisionExceptionTest.java:");
    }
  }

  public void testBindToProviderInstanceExceptions() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(D.class).toProvider(new DProvider());
        }
      }).getInstance(D.class);
      fail();
    } catch (ProvisionException e) {
      assertTrue(e.getCause() instanceof UnsupportedOperationException);
      assertContains(e.getMessage(),
          "1) Error in custom provider, java.lang.UnsupportedOperationException",
          "at " + ProvisionExceptionTest.class.getName(),
          getDeclaringSourcePart(getClass()));
    }
  }

  /**
   * This test demonstrates that if the user throws a ProvisionException, we wrap it to add context.
   */
  public void testProvisionExceptionsAreWrappedForBindToType() {
    try {
      Guice.createInjector().getInstance(F.class);
      fail();
    } catch (ProvisionException e) {
      assertContains(e.getMessage(), "1) User Exception",
          "at " + F.class.getName() + ".<init>(ProvisionExceptionTest.java:");
    }
  }

  public void testProvisionExceptionsAreWrappedForBindToProviderType() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(F.class).toProvider(FProvider.class);
        }
      }).getInstance(F.class);
      fail();
    } catch (ProvisionException e) {
      assertContains(e.getMessage(), "1) User Exception",
          "while locating ", FProvider.class.getName(),
          "while locating ", F.class.getName());
    }
  }

  public void testProvisionExceptionsAreWrappedForBindToProviderInstance() {
    try {
      Guice.createInjector(new AbstractModule() {
        protected void configure() {
          bind(F.class).toProvider(new FProvider());
        }
      }).getInstance(F.class);
      fail();
    } catch (ProvisionException e) {
      assertContains(e.getMessage(), "1) User Exception",
          "at " + ProvisionExceptionTest.class.getName(),
          getDeclaringSourcePart(getClass()));
    }
  }

  public void testProvisionExceptionIsSerializable() throws IOException {
    try {
      Guice.createInjector().getInstance(A.class);
      fail();
    } catch (ProvisionException expected) {
      ProvisionException reserialized = reserialize(expected);
      assertContains(reserialized.getMessage(),
          "1) Error injecting constructor, java.lang.UnsupportedOperationException",
              "at com.google.inject.ProvisionExceptionTest$RealD.<init>()",
              "at Key[type=com.google.inject.ProvisionExceptionTest$RealD, annotation=[none]]",
              "@com.google.inject.ProvisionExceptionTest$C.setD()[0]",
              "at Key[type=com.google.inject.ProvisionExceptionTest$C, annotation=[none]]",
              "@com.google.inject.ProvisionExceptionTest$B.c",
              "at Key[type=com.google.inject.ProvisionExceptionTest$B, annotation=[none]]",
              "@com.google.inject.ProvisionExceptionTest$A.<init>()[0]",
              "at Key[type=com.google.inject.ProvisionExceptionTest$A, annotation=[none]]");
    }
  }

  public void testMultipleCauses() {
    try {
      Guice.createInjector().getInstance(G.class);
      fail();
    } catch (ProvisionException e) {
      assertContains(e.getMessage(),
          "1) Error injecting method, java.lang.IllegalArgumentException",
          "Caused by: java.lang.IllegalArgumentException: java.lang.UnsupportedOperationException",
          "Caused by: java.lang.UnsupportedOperationException: Unsupported",
          "2) Error injecting method, java.lang.NullPointerException: can't inject second either",
          "Caused by: java.lang.NullPointerException: can't inject second either",
          "2 errors");
    }
  }

  public void testInjectInnerClass() throws Exception {
    Injector injector = Guice.createInjector();
    try {
      injector.getInstance(InnerClass.class);
      fail();
    } catch (Exception expected) {
      assertContains(expected.getMessage(),
          "Injecting into inner classes is not supported.",
          "while locating " + InnerClass.class.getName());
    }
  }

  public void testInjectLocalClass() throws Exception {
    class LocalClass {}

    Injector injector = Guice.createInjector();
    try {
      injector.getInstance(LocalClass.class);
      fail();
    } catch (Exception expected) {
      assertContains(expected.getMessage(),
          "Injecting into inner classes is not supported.",
          "while locating " + LocalClass.class.getName());
    }
  }

  public void testBindingAnnotationsOnMethodsAndConstructors() {
    try {
      Injector injector = Guice.createInjector();
      injector.getInstance(MethodWithBindingAnnotation.class);
      fail();
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(), MethodWithBindingAnnotation.class.getName()
          + ".injectMe() is annotated with @", Green.class.getName() + "(), ",
          "but binding annotations should be applied to its parameters instead.",
          "while locating " + MethodWithBindingAnnotation.class.getName());
    }

    try {
      Guice.createInjector().getInstance(ConstructorWithBindingAnnotation.class);
      fail();
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(), ConstructorWithBindingAnnotation.class.getName()
          + ".<init>() is annotated with @", Green.class.getName() + "(), ",
          "but binding annotations should be applied to its parameters instead.",
          "at " + ConstructorWithBindingAnnotation.class.getName() + ".class",
          "while locating " + ConstructorWithBindingAnnotation.class.getName());
    }
  }

  public void testBindingAnnotationWarningForScala() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(String.class).annotatedWith(Green.class).toInstance("lime!");
      }
    });
    injector.getInstance(LikeScala.class);
  }

  public void testLinkedBindings() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(D.class).to(RealD.class);
      }
    });

    try {
      injector.getInstance(D.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "at " + RealD.class.getName() + ".<init>(ProvisionExceptionTest.java:",
          "while locating " + RealD.class.getName(),
          "while locating " + D.class.getName());
    }
  }

  public void testProviderKeyBindings() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        bind(D.class).toProvider(DProvider.class);
      }
    });

    try {
      injector.getInstance(D.class);
      fail();
    } catch (ProvisionException expected) {
      assertContains(expected.getMessage(),
          "while locating " + DProvider.class.getName(),
          "while locating " + D.class.getName());
    }
  }

  private class InnerClass {}

  static class A {
    @Inject
    A(B b) { }
  }
  static class B {
    @Inject C c;
  }
  static class C {
    @Inject
    void setD(RealD d) { }
  }
  static class E {
    @Inject void setObject(Object o) {
      throw new UnsupportedOperationException();
    }
  }

  static class MethodWithBindingAnnotation {
    @Inject @Green void injectMe(String greenString) {}
  }

  static class ConstructorWithBindingAnnotation {
    // Suppress compiler errors by the error-prone checker InjectedConstructorAnnotations,
    // which catches injected constructors with binding annotations.
    @SuppressWarnings("InjectedConstructorAnnotations")
    @Inject @Green ConstructorWithBindingAnnotation(String greenString) {}
  }

  /**
   * In Scala, fields automatically get accessor methods with the same name. So we don't do
   * misplaced-binding annotation detection if the offending method has a matching field.
   */
  static class LikeScala {
    @Inject @Green String green;
    @Inject @Green String green() { return green; }
  }

  @Retention(RUNTIME)
  @Target({ FIELD, PARAMETER, CONSTRUCTOR, METHOD })
  @BindingAnnotation
  @interface Green {}

  interface D {}

  static class RealD implements D {
    @Inject RealD() {
      throw new UnsupportedOperationException();
    }
  }

  static class DProvider implements Provider<D> {
    public D get() {
      throw new UnsupportedOperationException();
    }
  }

  static class F {
    @Inject public F() {
      throw new ProvisionException("User Exception", new RuntimeException());
    }
  }

  static class FProvider implements Provider<F> {
    public F get() {
      return new F();
    }
  }

  static class G {
    @Inject void injectFirst() {
      throw new IllegalArgumentException(new UnsupportedOperationException("Unsupported"));
    }
    @Inject void injectSecond() {
      throw new NullPointerException("can't inject second either");
    }
  }
}
