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

import static com.google.inject.name.Names.named;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.collect.ImmutableMap;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
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.Provides;
import com.google.inject.ScopeAnnotation;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;
import com.google.inject.spi.PrivateElements;
import com.google.inject.util.Providers;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Map;
import junit.framework.TestCase;

/**
 * Tests for {@link ServletScopes}.
 *
 * @author forster@google.com (Mike Forster)
 */
public class ServletScopesTest extends TestCase {
  public void testIsRequestScopedPositive() {
    final Key<String> a = Key.get(String.class, named("A"));
    final Key<String> b = Key.get(String.class, named("B"));
    final Key<String> c = Key.get(String.class, named("C"));
    final Key<String> d = Key.get(String.class, named("D"));
    final Key<Object> e = Key.get(Object.class, named("E"));
    final Key<String> f = Key.get(String.class, named("F"));
    final Key<String> g = Key.get(String.class, named("G"));

    Module requestScopedBindings =
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(a).to(b);
            bind(b).to(c);
            bind(c).toProvider(Providers.of("c")).in(ServletScopes.REQUEST);
            bind(d).toProvider(Providers.of("d")).in(RequestScoped.class);
            bind(e).to(AnnotatedRequestScopedClass.class);
            install(
                new PrivateModule() {
                  @Override
                  protected void configure() {
                    bind(f).toProvider(Providers.of("f")).in(RequestScoped.class);
                    expose(f);
                  }
                });
          }

          @Provides
          @Named("G")
          @RequestScoped
          String provideG() {
            return "g";
          }
        };

    @SuppressWarnings("unchecked") // we know the module contains only bindings
    List<Element> moduleBindings = Elements.getElements(requestScopedBindings);
    ImmutableMap<Key<?>, Binding<?>> map = indexBindings(moduleBindings);
    // linked bindings are not followed by modules
    assertFalse(ServletScopes.isRequestScoped(map.get(a)));
    assertFalse(ServletScopes.isRequestScoped(map.get(b)));
    assertTrue(ServletScopes.isRequestScoped(map.get(c)));
    assertTrue(ServletScopes.isRequestScoped(map.get(d)));
    // annotated classes are not followed by modules
    assertFalse(ServletScopes.isRequestScoped(map.get(e)));
    assertTrue(ServletScopes.isRequestScoped(map.get(f)));
    assertTrue(ServletScopes.isRequestScoped(map.get(g)));

    Injector injector = Guice.createInjector(requestScopedBindings, new ServletModule());
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(a)));
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(b)));
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(c)));
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(d)));
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(e)));
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(f)));
    assertTrue(ServletScopes.isRequestScoped(injector.getBinding(g)));
  }

  public void testIsRequestScopedNegative() {
    final Key<String> a = Key.get(String.class, named("A"));
    final Key<String> b = Key.get(String.class, named("B"));
    final Key<String> c = Key.get(String.class, named("C"));
    final Key<String> d = Key.get(String.class, named("D"));
    final Key<String> e = Key.get(String.class, named("E"));
    final Key<String> f = Key.get(String.class, named("F"));
    final Key<String> g = Key.get(String.class, named("G"));
    final Key<String> h = Key.get(String.class, named("H"));
    final Key<String> i = Key.get(String.class, named("I"));
    final Key<String> j = Key.get(String.class, named("J"));

    Module requestScopedBindings =
        new AbstractModule() {
          @Override
          protected void configure() {
            bind(a).to(b);
            bind(b).to(c);
            bind(c).toProvider(Providers.of("c")).in(Scopes.NO_SCOPE);
            bind(d).toInstance("d");
            bind(e).toProvider(Providers.of("e")).asEagerSingleton();
            bind(f).toProvider(Providers.of("f")).in(Scopes.SINGLETON);
            bind(g).toProvider(Providers.of("g")).in(Singleton.class);
            bind(h).toProvider(Providers.of("h")).in(CustomScoped.class);
            bindScope(CustomScoped.class, Scopes.NO_SCOPE);
            install(
                new PrivateModule() {
                  @Override
                  protected void configure() {
                    bind(i).toProvider(Providers.of("i")).in(CustomScoped.class);
                    expose(i);
                  }
                });
          }

          @Provides
          @Named("J")
          @CustomScoped
          String provideJ() {
            return "j";
          }
        };

    @SuppressWarnings("unchecked") // we know the module contains only bindings
    List<Element> moduleBindings = Elements.getElements(requestScopedBindings);
    ImmutableMap<Key<?>, Binding<?>> map = indexBindings(moduleBindings);
    assertFalse(ServletScopes.isRequestScoped(map.get(a)));
    assertFalse(ServletScopes.isRequestScoped(map.get(b)));
    assertFalse(ServletScopes.isRequestScoped(map.get(c)));
    assertFalse(ServletScopes.isRequestScoped(map.get(d)));
    assertFalse(ServletScopes.isRequestScoped(map.get(e)));
    assertFalse(ServletScopes.isRequestScoped(map.get(f)));
    assertFalse(ServletScopes.isRequestScoped(map.get(g)));
    assertFalse(ServletScopes.isRequestScoped(map.get(h)));
    assertFalse(ServletScopes.isRequestScoped(map.get(i)));
    assertFalse(ServletScopes.isRequestScoped(map.get(j)));

    Injector injector = Guice.createInjector(requestScopedBindings);
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(a)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(b)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(c)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(d)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(e)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(f)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(g)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(h)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(i)));
    assertFalse(ServletScopes.isRequestScoped(injector.getBinding(j)));
  }

  @RequestScoped
  static class AnnotatedRequestScopedClass {}

  @Target({ElementType.TYPE, ElementType.METHOD})
  @Retention(RUNTIME)
  @ScopeAnnotation
  private @interface CustomScoped {}

  private ImmutableMap<Key<?>, Binding<?>> indexBindings(Iterable<Element> elements) {
    ImmutableMap.Builder<Key<?>, Binding<?>> builder = ImmutableMap.builder();
    for (Element element : elements) {
      if (element instanceof Binding) {
        Binding<?> binding = (Binding<?>) element;
        builder.put(binding.getKey(), binding);
      } else if (element instanceof PrivateElements) {
        PrivateElements privateElements = (PrivateElements) element;
        Map<Key<?>, Binding<?>> privateBindings = indexBindings(privateElements.getElements());
        for (Key<?> exposed : privateElements.getExposedKeys()) {
          builder.put(exposed, privateBindings.get(exposed));
        }
      }
    }
    return builder.buildOrThrow();
  }
}
