/*
 * Copyright (C) 2015 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.inject.Asserts.assertContains;
import static com.google.inject.name.Names.named;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Binding;
import com.google.inject.CreationException;
import com.google.inject.Exposed;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.PrivateModule;
import com.google.inject.internal.util.StackTraceElements;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Set;
import junit.framework.TestCase;

/** Tests for {@link ModuleAnnotatedMethodScanner} usage. */
public class ModuleAnnotatedMethodScannerTest extends TestCase {

  public void testScanning() throws Exception {
    Module module =
        new AbstractModule() {

          @TestProvides
          @Named("foo")
          String foo() {
            return "foo";
          }

          @TestProvides
          @Named("foo2")
          String foo2() {
            return "foo2";
          }
        };
    Injector injector = Guice.createInjector(module, NamedMunger.module());

    // assert no bindings named "foo" or "foo2" exist -- they were munged.
    assertMungedBinding(injector, String.class, "foo", "foo");
    assertMungedBinding(injector, String.class, "foo2", "foo2");

    Binding<String> fooBinding = injector.getBinding(Key.get(String.class, named("foo-munged")));
    Binding<String> foo2Binding = injector.getBinding(Key.get(String.class, named("foo2-munged")));
    // Validate the provider has a sane toString
    assertEquals(
        methodName(TestProvides.class, "foo", module), fooBinding.getProvider().toString());
    assertEquals(
        methodName(TestProvides.class, "foo2", module), foo2Binding.getProvider().toString());
  }

  public void testSkipSources() throws Exception {
    Module module =
        new AbstractModule() {
          @Override
          protected void configure() {
            binder()
                .skipSources(getClass())
                .install(
                    new AbstractModule() {

                      @TestProvides
                      @Named("foo")
                      String foo() {
                        return "foo";
                      }
                    });
          }
        };
    Injector injector = Guice.createInjector(module, NamedMunger.module());
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testWithSource() throws Exception {
    Module module =
        new AbstractModule() {
          @Override
          protected void configure() {
            binder()
                .withSource("source")
                .install(
                    new AbstractModule() {

                      @TestProvides
                      @Named("foo")
                      String foo() {
                        return "foo";
                      }
                    });
          }
        };
    Injector injector = Guice.createInjector(module, NamedMunger.module());
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testMoreThanOneClaimedAnnotationFails() throws Exception {
    Module module =
        new AbstractModule() {

          @TestProvides
          @TestProvides2
          String foo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(module, NamedMunger.module());
      fail();
    } catch (CreationException expected) {
      assertEquals(1, expected.getErrorMessages().size());
      assertContains(
          expected.getMessage(),
          "More than one annotation claimed by NamedMunger on method "
              + module.getClass().getName()
              + ".foo(). Methods can only have "
              + "one annotation claimed per scanner.");
    }
  }

  private String methodName(Class<? extends Annotation> annotation, String method, Object container)
      throws Exception {
    return "@"
        + annotation.getName()
        + " "
        + StackTraceElements.forMember(container.getClass().getDeclaredMethod(method));
  }

  @Documented
  @Target(METHOD)
  @Retention(RUNTIME)
  private @interface TestProvides {}

  @Documented
  @Target(METHOD)
  @Retention(RUNTIME)
  private @interface TestProvides2 {}

  private static class NamedMunger extends ModuleAnnotatedMethodScanner {
    static Module module() {
      return new AbstractModule() {
        @Override
        protected void configure() {
          binder().scanModulesForAnnotatedMethods(new NamedMunger());
        }
      };
    }

    @Override
    public String toString() {
      return "NamedMunger";
    }

    @Override
    public Set<? extends Class<? extends Annotation>> annotationClasses() {
      return ImmutableSet.of(TestProvides.class, TestProvides2.class);
    }

    @Override
    public <T> Key<T> prepareMethod(
        Binder binder, Annotation annotation, Key<T> key, InjectionPoint injectionPoint) {
      return Key.get(
          key.getTypeLiteral(), Names.named(((Named) key.getAnnotation()).value() + "-munged"));
    }
  }

  private void assertMungedBinding(
      Injector injector, Class<?> clazz, String originalName, Object expectedValue) {
    assertNull(injector.getExistingBinding(Key.get(clazz, named(originalName))));
    Binding<?> fooBinding = injector.getBinding(Key.get(clazz, named(originalName + "-munged")));
    assertEquals(expectedValue, fooBinding.getProvider().get());
  }

  public void testFailingScanner() {
    try {
      Guice.createInjector(new SomeModule(), FailingScanner.module());
      fail();
    } catch (CreationException expected) {
      Message m = Iterables.getOnlyElement(expected.getErrorMessages());
      assertEquals(
          "An exception was caught and reported. Message: Failing in the scanner.", m.getMessage());
      assertEquals(IllegalStateException.class, m.getCause().getClass());
      ElementSource source = (ElementSource) Iterables.getOnlyElement(m.getSources());
      assertEquals(
          SomeModule.class.getName(), Iterables.getOnlyElement(source.getModuleClassNames()));
      assertEquals(
          String.class.getName() + " " + SomeModule.class.getName() + ".aString()",
          source.toString());
    }
  }

  public static class FailingScanner extends ModuleAnnotatedMethodScanner {
    static Module module() {
      return new AbstractModule() {
        @Override
        protected void configure() {
          binder().scanModulesForAnnotatedMethods(new FailingScanner());
        }
      };
    }

    @Override
    public Set<? extends Class<? extends Annotation>> annotationClasses() {
      return ImmutableSet.of(TestProvides.class);
    }

    @Override
    public <T> Key<T> prepareMethod(
        Binder binder, Annotation rawAnnotation, Key<T> key, InjectionPoint injectionPoint) {
      throw new IllegalStateException("Failing in the scanner.");
    }
  }

  static class SomeModule extends AbstractModule {
    @TestProvides
    String aString() {
      return "Foo";
    }

  }

  public void testChildInjectorInheritsScanner() {
    Injector parent = Guice.createInjector(NamedMunger.module());
    Injector child =
        parent.createChildInjector(
            new AbstractModule() {

              @TestProvides
              @Named("foo")
              String foo() {
                return "foo";
              }
            });
    assertMungedBinding(child, String.class, "foo", "foo");
  }

  public void testChildInjectorScannersDontImpactSiblings() {
    Module module =
        new AbstractModule() {

          @TestProvides
          @Named("foo")
          String foo() {
            return "foo";
          }
        };
    Injector parent = Guice.createInjector();
    Injector child = parent.createChildInjector(NamedMunger.module(), module);
    assertMungedBinding(child, String.class, "foo", "foo");

    // no foo nor foo-munged in sibling, since scanner never saw it.
    Injector sibling = parent.createChildInjector(module);
    assertNull(sibling.getExistingBinding(Key.get(String.class, named("foo"))));
    assertNull(sibling.getExistingBinding(Key.get(String.class, named("foo-munged"))));
  }

  public void testPrivateModuleInheritScanner_usingPrivateModule() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new PrivateModule() {
              @Override
              protected void configure() {}

              @Exposed
              @TestProvides
              @Named("foo")
              String foo() {
                return "foo";
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModule_skipSourcesWithinPrivateModule() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new PrivateModule() {
              @Override
              protected void configure() {
                binder()
                    .skipSources(getClass())
                    .install(
                        new AbstractModule() {

                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModule_skipSourcesForPrivateModule() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new AbstractModule() {
              @Override
              protected void configure() {
                binder()
                    .skipSources(getClass())
                    .install(
                        new PrivateModule() {
                          @Override
                          protected void configure() {}

                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModuleInheritScanner_usingPrivateBinder() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new AbstractModule() {
              @Override
              protected void configure() {
                binder()
                    .newPrivateBinder()
                    .install(
                        new AbstractModule() {

                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModuleInheritScanner_skipSourcesFromPrivateBinder() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new AbstractModule() {
              @Override
              protected void configure() {
                binder()
                    .newPrivateBinder()
                    .skipSources(getClass())
                    .install(
                        new AbstractModule() {

                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModuleInheritScanner_skipSourcesFromPrivateBinder2() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new AbstractModule() {
              @Override
              protected void configure() {
                binder()
                    .skipSources(getClass())
                    .newPrivateBinder()
                    .install(
                        new AbstractModule() {

                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModuleScannersDontImpactSiblings_usingPrivateModule() {
    Injector injector =
        Guice.createInjector(
            new PrivateModule() {
              @Override
              protected void configure() {
                install(NamedMunger.module());
              }

              @Exposed
              @TestProvides
              @Named("foo")
              String foo() {
                return "foo";
              }
            },
            new PrivateModule() {
              @Override
              protected void configure() {}

              // ignored! (because the scanner doesn't run over this module)
              @Exposed
              @TestProvides
              @Named("foo")
              String foo() {
                return "foo";
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModuleScannersDontImpactSiblings_usingPrivateBinder() {
    Injector injector =
        Guice.createInjector(
            new AbstractModule() {
              @Override
              protected void configure() {
                binder()
                    .newPrivateBinder()
                    .install(
                        new AbstractModule() {
                          @Override
                          protected void configure() {
                            install(NamedMunger.module());
                          }

                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            },
            new AbstractModule() {
              @Override
              protected void configure() {
                binder()
                    .newPrivateBinder()
                    .install(
                        new AbstractModule() {

                          // ignored! (because the scanner doesn't run over this module)
                          @Exposed
                          @TestProvides
                          @Named("foo")
                          String foo() {
                            return "foo";
                          }
                        });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }

  public void testPrivateModuleWithinPrivateModule() {
    Injector injector =
        Guice.createInjector(
            NamedMunger.module(),
            new PrivateModule() {
              @Override
              protected void configure() {
                expose(Key.get(String.class, named("foo-munged")));
                install(
                    new PrivateModule() {
                      @Override
                      protected void configure() {}

                      @Exposed
                      @TestProvides
                      @Named("foo")
                      String foo() {
                        return "foo";
                      }
                    });
              }
            });
    assertMungedBinding(injector, String.class, "foo", "foo");
  }
}
