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

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

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.multibindings.ProvidesIntoOptional.Type;
import com.google.inject.name.Named;
import java.lang.annotation.Retention;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import junit.framework.TestCase;

/**
 * Tests the various @ProvidesInto annotations.
 *
 * @author sameb@google.com (Sam Berlin)
 */
public class ProvidesIntoTest extends TestCase {

  public void testAnnotation() throws Exception {
    Injector injector =
        Guice.createInjector(
            MultibindingsScanner.asModule(),
            new AbstractModule() {

              @ProvidesIntoSet
              @Named("foo")
              String setFoo() {
                return "foo";
              }

              @ProvidesIntoSet
              @Named("foo")
              String setFoo2() {
                return "foo2";
              }

              @ProvidesIntoSet
              @Named("bar")
              String setBar() {
                return "bar";
              }

              @ProvidesIntoSet
              @Named("bar")
              String setBar2() {
                return "bar2";
              }

              @ProvidesIntoSet
              String setNoAnnotation() {
                return "na";
              }

              @ProvidesIntoSet
              String setNoAnnotation2() {
                return "na2";
              }

              @ProvidesIntoMap
              @StringMapKey("fooKey")
              @Named("foo")
              String mapFoo() {
                return "foo";
              }

              @ProvidesIntoMap
              @StringMapKey("foo2Key")
              @Named("foo")
              String mapFoo2() {
                return "foo2";
              }

              @ProvidesIntoMap
              @ClassMapKey(String.class)
              @Named("bar")
              String mapBar() {
                return "bar";
              }

              @ProvidesIntoMap
              @ClassMapKey(Number.class)
              @Named("bar")
              String mapBar2() {
                return "bar2";
              }

              @ProvidesIntoMap
              @TestEnumKey(TestEnum.A)
              String mapNoAnnotation() {
                return "na";
              }

              @ProvidesIntoMap
              @TestEnumKey(TestEnum.B)
              String mapNoAnnotation2() {
                return "na2";
              }

              @ProvidesIntoMap
              @WrappedKey(number = 1)
              Number wrapped1() {
                return 11;
              }

              @ProvidesIntoMap
              @WrappedKey(number = 2)
              Number wrapped2() {
                return 22;
              }

              @ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT)
              @Named("foo")
              String optionalDefaultFoo() {
                return "foo";
              }

              @ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL)
              @Named("foo")
              String optionalActualFoo() {
                return "foo2";
              }

              @ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT)
              @Named("bar")
              String optionalDefaultBar() {
                return "bar";
              }

              @ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL)
              String optionalActualBar() {
                return "na2";
              }
            });

    Set<String> fooSet = injector.getInstance(new Key<Set<String>>(named("foo")) {});
    assertEquals(ImmutableSet.of("foo", "foo2"), fooSet);

    Set<String> barSet = injector.getInstance(new Key<Set<String>>(named("bar")) {});
    assertEquals(ImmutableSet.of("bar", "bar2"), barSet);

    Set<String> noAnnotationSet = injector.getInstance(new Key<Set<String>>() {});
    assertEquals(ImmutableSet.of("na", "na2"), noAnnotationSet);

    Map<String, String> fooMap =
        injector.getInstance(new Key<Map<String, String>>(named("foo")) {});
    assertEquals(ImmutableMap.of("fooKey", "foo", "foo2Key", "foo2"), fooMap);

    Map<Class<?>, String> barMap =
        injector.getInstance(new Key<Map<Class<?>, String>>(named("bar")) {});
    assertEquals(ImmutableMap.of(String.class, "bar", Number.class, "bar2"), barMap);

    Map<TestEnum, String> noAnnotationMap =
        injector.getInstance(new Key<Map<TestEnum, String>>() {});
    assertEquals(ImmutableMap.of(TestEnum.A, "na", TestEnum.B, "na2"), noAnnotationMap);

    Map<WrappedKey, Number> wrappedMap =
        injector.getInstance(new Key<Map<WrappedKey, Number>>() {});
    assertEquals(ImmutableMap.of(wrappedKeyFor(1), 11, wrappedKeyFor(2), 22), wrappedMap);

    Optional<String> fooOptional = injector.getInstance(new Key<Optional<String>>(named("foo")) {});
    assertEquals("foo2", fooOptional.get());

    Optional<String> barOptional = injector.getInstance(new Key<Optional<String>>(named("bar")) {});
    assertEquals("bar", barOptional.get());

    Optional<String> noAnnotationOptional = injector.getInstance(new Key<Optional<String>>() {});
    assertEquals("na2", noAnnotationOptional.get());
  }

  enum TestEnum {
    A,
    B
  }

  @MapKey(unwrapValue = true)
  @Retention(RUNTIME)
  @interface TestEnumKey {
    TestEnum value();
  }

  @MapKey(unwrapValue = false)
  @Retention(RUNTIME)
  @interface WrappedKey {
    int number();
  }

  @SuppressWarnings("unused")
  @WrappedKey(number = 1)
  private static Object wrappedKey1Holder;

  @SuppressWarnings("unused")
  @WrappedKey(number = 2)
  private static Object wrappedKey2Holder;

  WrappedKey wrappedKeyFor(int number) throws Exception {
    Field field;
    switch (number) {
      case 1:
        field = ProvidesIntoTest.class.getDeclaredField("wrappedKey1Holder");
        break;
      case 2:
        field = ProvidesIntoTest.class.getDeclaredField("wrappedKey2Holder");
        break;
      default:
        throw new IllegalArgumentException("only 1 or 2 supported");
    }
    return field.getAnnotation(WrappedKey.class);
  }

  public void testDoubleScannerIsIgnored() {
    Injector injector =
        Guice.createInjector(
            MultibindingsScanner.asModule(),
            MultibindingsScanner.asModule(),
            new AbstractModule() {

              @ProvidesIntoSet
              String provideFoo() {
                return "foo";
              }
            });
    assertEquals(ImmutableSet.of("foo"), injector.getInstance(new Key<Set<String>>() {}));
  }

  @MapKey(unwrapValue = true)
  @Retention(RUNTIME)
  @interface ArrayUnwrappedKey {
    int[] value();
  }

  public void testArrayKeys_unwrapValuesTrue() {
    Module m =
        new AbstractModule() {

          @ProvidesIntoMap
          @ArrayUnwrappedKey({1, 2})
          String provideFoo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(MultibindingsScanner.asModule(), m);
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertContains(
          ce.getMessage(),
          "Array types are not allowed in a MapKey with unwrapValue=true:"
              + " ProvidesIntoTest$ArrayUnwrappedKey",
          "at ProvidesIntoTest$14.provideFoo");
    }
  }

  @MapKey(unwrapValue = false)
  @Retention(RUNTIME)
  @interface ArrayWrappedKey {
    int[] number();
  }

  @SuppressWarnings("unused")
  @ArrayWrappedKey(number = {1, 2})
  private static Object arrayWrappedKeyHolder12;

  @SuppressWarnings("unused")
  @ArrayWrappedKey(number = {3, 4})
  private static Object arrayWrappedKeyHolder34;

  ArrayWrappedKey arrayWrappedKeyFor(int number) throws Exception {
    Field field;
    switch (number) {
      case 12:
        field = ProvidesIntoTest.class.getDeclaredField("arrayWrappedKeyHolder12");
        break;
      case 34:
        field = ProvidesIntoTest.class.getDeclaredField("arrayWrappedKeyHolder34");
        break;
      default:
        throw new IllegalArgumentException("only 1 or 2 supported");
    }
    return field.getAnnotation(ArrayWrappedKey.class);
  }

  public void testArrayKeys_unwrapValuesFalse() throws Exception {
    Module m =
        new AbstractModule() {

          @ProvidesIntoMap
          @ArrayWrappedKey(number = {1, 2})
          String provideFoo() {
            return "foo";
          }

          @ProvidesIntoMap
          @ArrayWrappedKey(number = {3, 4})
          String provideBar() {
            return "bar";
          }
        };
    Injector injector = Guice.createInjector(MultibindingsScanner.asModule(), m);
    Map<ArrayWrappedKey, String> map =
        injector.getInstance(new Key<Map<ArrayWrappedKey, String>>() {});
    ArrayWrappedKey key12 = arrayWrappedKeyFor(12);
    ArrayWrappedKey key34 = arrayWrappedKeyFor(34);
    assertEquals("foo", map.get(key12));
    assertEquals("bar", map.get(key34));
    assertEquals(2, map.size());
  }

  public void testProvidesIntoSetWithMapKey() {
    Module m =
        new AbstractModule() {

          @ProvidesIntoSet
          @TestEnumKey(TestEnum.A)
          String provideFoo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(MultibindingsScanner.asModule(), m);
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertContains(
          ce.getMessage(),
          "Found a MapKey annotation on non map binding at ProvidesIntoTest$17.provideFoo()");
    }
  }

  public void testProvidesIntoOptionalWithMapKey() {
    Module m =
        new AbstractModule() {

          @ProvidesIntoOptional(Type.ACTUAL)
          @TestEnumKey(TestEnum.A)
          String provideFoo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(MultibindingsScanner.asModule(), m);
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertContains(
          ce.getMessage(),
          "Found a MapKey annotation on non map binding at ProvidesIntoTest$18.provideFoo");
    }
  }

  public void testProvidesIntoMapWithoutMapKey() {
    Module m =
        new AbstractModule() {

          @ProvidesIntoMap
          String provideFoo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(MultibindingsScanner.asModule(), m);
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertContains(
          ce.getMessage(), "No MapKey found for map binding at ProvidesIntoTest$19.provideFoo()");
    }
  }

  @MapKey(unwrapValue = true)
  @Retention(RUNTIME)
  @interface TestEnumKey2 {
    TestEnum value();
  }

  public void testMoreThanOneMapKeyAnnotation() {
    Module m =
        new AbstractModule() {

          @ProvidesIntoMap
          @TestEnumKey(TestEnum.A)
          @TestEnumKey2(TestEnum.B)
          String provideFoo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(MultibindingsScanner.asModule(), m);
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertContains(
          ce.getMessage(),
          "Found more than one MapKey annotations on ProvidesIntoTest$20.provideFoo().");
    }
  }

  @MapKey(unwrapValue = true)
  @Retention(RUNTIME)
  @interface MissingValueMethod {}

  public void testMapKeyMissingValueMethod() {
    Module m =
        new AbstractModule() {

          @ProvidesIntoMap
          @MissingValueMethod
          String provideFoo() {
            return "foo";
          }
        };
    try {
      Guice.createInjector(MultibindingsScanner.asModule(), m);
      fail();
    } catch (CreationException ce) {
      assertEquals(1, ce.getErrorMessages().size());
      assertContains(
          ce.getMessage(),
          "No 'value' method in MapKey with unwrapValue=true: ProvidesIntoTest$MissingValueMethod");
    }
  }
}
