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

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.matcher.Matchers;

import junit.framework.TestCase;

import org.aopalliance.intercept.MethodInterceptor;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Tests for interception of default methods.
 *
 * @author cgdecker@google.com (Colin Decker)
 */
public class DefaultMethodInterceptionTest extends TestCase {

  private static final AtomicInteger callCount = new AtomicInteger(0);
  private static final AtomicInteger interceptedCallCount = new AtomicInteger(0);

  // the interceptor's a lambda too
  private final MethodInterceptor interceptor = invocation -> {
    interceptedCallCount.incrementAndGet();
    return invocation.proceed();
  };

  @Override
  protected void setUp() throws Exception {
    callCount.set(0);
    interceptedCallCount.set(0);
  }

  @Retention(RUNTIME)
  @Target({METHOD, TYPE})
  public @interface InterceptMe {}

  /** Interface with a default method annotated to be intercepted. */
  public interface Foo {
    @InterceptMe
    default String defaultMethod() {
      callCount.incrementAndGet();
      return "Foo";
    }
  }

  /** Foo implementation that does not override the default method. */
  public static class NonOverridingFoo implements Foo {
    public String methodCallingDefault() {
      return "NonOverriding-" + defaultMethod();
    }
  }

  public void testInterceptedDefaultMethod() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(InterceptMe.class),
            interceptor);
        bind(Foo.class).to(NonOverridingFoo.class);
      }
    });

    Foo foo = injector.getInstance(Foo.class);
    assertEquals("Foo", foo.defaultMethod());
    assertEquals(1, callCount.get());
    assertEquals(1, interceptedCallCount.get());
  }

  public void testInterceptedDefaultMethod_calledByAnotherMethod() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(InterceptMe.class),
            interceptor);
      }
    });

    NonOverridingFoo foo = injector.getInstance(NonOverridingFoo.class);
    assertEquals("NonOverriding-Foo", foo.methodCallingDefault());
    assertEquals(1, callCount.get());
    assertEquals(1, interceptedCallCount.get());
  }

  /** A base class defining a method with the same signature as Foo's default method. */
  public static class BaseClass {
    // the definition of this method on the class will win over the default method
    public String defaultMethod() {
      callCount.incrementAndGet();
      return "BaseClass";
    }
  }

  /** Foo implementation that should use superclass method rather than default method. */
  public static class InheritingFoo extends BaseClass implements Foo {
  }

  public void testInterceptedDefaultMethod_whenParentClassDefinesNonInterceptedMethod() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(InterceptMe.class),
            interceptor);
        bind(Foo.class).to(InheritingFoo.class);
      }
    });

    // the concrete implementation that wins is not annotated
    Foo foo = injector.getInstance(Foo.class);
    assertEquals("BaseClass", foo.defaultMethod());
    assertEquals(1, callCount.get());
    assertEquals(0, interceptedCallCount.get());
  }

  /**
   * A base class defining an intercepted method with the same signature as Foo's default method.
   */
  public static class BaseClass2 {
    // the definition of this method on the class will win over the default method
    @InterceptMe
    public String defaultMethod() {
      callCount.incrementAndGet();
      return "BaseClass2";
    }
  }

  /**
   * Foo implementation that should use intercepted superclass method rather than default method.
   */
  public static class InheritingFoo2 extends BaseClass2 implements Foo {
  }

  public void testInterceptedDefaultMethod_whenParentClassDefinesInterceptedMethod() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(InterceptMe.class),
            interceptor);
        bind(Foo.class).to(InheritingFoo2.class);
      }
    });

    // the concrete implementation that wins is not annotated
    Foo foo = injector.getInstance(Foo.class);
    assertEquals("BaseClass2", foo.defaultMethod());
    assertEquals(1, callCount.get());
    assertEquals(1, interceptedCallCount.get());
  }

  public interface Baz {
    default String doSomething() {
      return "Baz";
    }

    String doSomethingElse();
  }

  public static class BazImpl implements Baz {

    @Override
    public String doSomethingElse() {
      return "BazImpl";
    }
  }

  public void testInterception_ofAllMethodsOnType() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindInterceptor(Matchers.subclassesOf(Baz.class), Matchers.any(), interceptor);
        bind(Baz.class).to(BazImpl.class);
      }
    });

    Baz baz = injector.getInstance(Baz.class);

    assertEquals("Baz", baz.doSomething());
    assertEquals("BazImpl", baz.doSomethingElse());

    assertEquals(2, interceptedCallCount.get());
  }

  public void testInterception_ofAllMethodsOnType_interceptsInheritedDefaultMethod() {
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bindInterceptor(Matchers.subclassesOf(BazImpl.class), Matchers.any(), interceptor);
        bind(Baz.class).to(BazImpl.class);
      }
    });

    Baz baz = injector.getInstance(Baz.class);

    assertEquals("Baz", baz.doSomething());
    assertEquals("BazImpl", baz.doSomethingElse());

    assertEquals(2, interceptedCallCount.get());
  }
}