/**
 * Copyright (C) 2008 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.name.Names.named;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.binder.ScopedBindingBuilder;
import com.google.inject.name.Named;
import com.google.inject.util.Providers;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

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

/**
 * @author jessewilson@google.com (Jesse Wilson)
 */
public class BinderTestSuite extends TestCase {

  public static Test suite() {
    TestSuite suite = new TestSuite();

    new Builder()
        .name("bind A")
        .module(new AbstractModule() {
          protected void configure() {
            bind(A.class);
          }
        })
        .creationException("No implementation for %s was bound", A.class.getName())
        .addToSuite(suite);

    new Builder()
        .name("bind PlainA named apple")
        .module(new AbstractModule() {
          protected void configure() {
            bind(PlainA.class).annotatedWith(named("apple"));
          }
        })
        .creationException("No implementation for %s annotated with %s was bound",
            PlainA.class.getName(), named("apple"))
        .addToSuite(suite);

    new Builder()
        .name("bind A to new PlainA(1)")
        .module(new AbstractModule() {
          protected void configure() {
            bind(A.class).toInstance(new PlainA(1));
          }
        })
        .creationTime(CreationTime.NONE)
        .expectedValues(new PlainA(1), new PlainA(1), new PlainA(1))
        .addToSuite(suite);

    new Builder()
        .name("no binding, AWithProvidedBy")
        .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class)
        .addToSuite(suite);

    new Builder()
        .name("no binding, AWithImplementedBy")
        .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class)
        .addToSuite(suite);

    new Builder()
        .name("no binding, ScopedA")
        .key(Key.get(ScopedA.class), InjectsScopedA.class)
        .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202))
        .addToSuite(suite);

    new Builder()
        .name("no binding, AWithProvidedBy named apple")
        .key(Key.get(AWithProvidedBy.class, named("apple")),
            InjectsAWithProvidedByNamedApple.class)
        .configurationException("No implementation for %s annotated with %s was bound",
            AWithProvidedBy.class.getName(), named("apple"))
        .addToSuite(suite);

    new Builder()
        .name("no binding, AWithImplementedBy named apple")
        .key(Key.get(AWithImplementedBy.class, named("apple")),
            InjectsAWithImplementedByNamedApple.class)
        .configurationException("No implementation for %s annotated with %s was bound",
            AWithImplementedBy.class.getName(), named("apple"))
        .addToSuite(suite);

    new Builder()
        .name("no binding, ScopedA named apple")
        .key(Key.get(ScopedA.class, named("apple")), InjectsScopedANamedApple.class)
        .configurationException("No implementation for %s annotated with %s was bound",
            ScopedA.class.getName(), named("apple"))
        .addToSuite(suite);

    for (final Scoper scoper : Scoper.values()) {
      new Builder()
          .name("bind PlainA")
          .key(Key.get(PlainA.class), InjectsPlainA.class)
          .module(new AbstractModule() {
            protected void configure() {
              AnnotatedBindingBuilder<PlainA> abb = bind(PlainA.class);
              scoper.configure(abb);
            }
          })
          .scoper(scoper)
          .addToSuite(suite);

      new Builder()
          .name("bind A to PlainA")
          .module(new AbstractModule() {
            protected void configure() {
              ScopedBindingBuilder sbb = bind(A.class).to(PlainA.class);
              scoper.configure(sbb);
            }
          })
          .scoper(scoper)
          .addToSuite(suite);

      new Builder()
          .name("bind A to PlainAProvider.class")
          .module(new AbstractModule() {
            protected void configure() {
              ScopedBindingBuilder sbb = bind(A.class).toProvider(PlainAProvider.class);
              scoper.configure(sbb);
            }
          })
          .scoper(scoper)
          .addToSuite(suite);

      new Builder()
          .name("bind A to new PlainAProvider()")
          .module(new AbstractModule() {
            protected void configure() {
              ScopedBindingBuilder sbb = bind(A.class).toProvider(new PlainAProvider());
              scoper.configure(sbb);
            }
          })
          .scoper(scoper)
          .addToSuite(suite);

      new Builder()
          .name("bind AWithProvidedBy")
          .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class)
          .module(new AbstractModule() {
            protected void configure() {
              ScopedBindingBuilder sbb = bind(AWithProvidedBy.class);
              scoper.configure(sbb);
            }
          })
          .scoper(scoper)
          .addToSuite(suite);

      new Builder()
          .name("bind AWithImplementedBy")
          .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class)
          .module(new AbstractModule() {
            protected void configure() {
              ScopedBindingBuilder sbb = bind(AWithImplementedBy.class);
              scoper.configure(sbb);
            }
          })
          .scoper(scoper)
          .addToSuite(suite);

      new Builder()
          .name("bind ScopedA")
          .key(Key.get(ScopedA.class), InjectsScopedA.class)
          .module(new AbstractModule() {
            protected void configure() {
              ScopedBindingBuilder sbb = bind(ScopedA.class);
              scoper.configure(sbb);
            }
          })
          .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202))
          .scoper(scoper)
          .addToSuite(suite);


      new Builder()
          .name("bind AWithProvidedBy named apple")
          .module(new AbstractModule() {
            protected void configure() {
              scoper.configure(bind(AWithProvidedBy.class).annotatedWith(named("apple")));
            }
          })
          .creationException("No implementation for %s annotated with %s was bound",
              AWithProvidedBy.class.getName(), named("apple"))
          .addToSuite(suite);

      new Builder()
          .name("bind AWithImplementedBy named apple")
          .module(new AbstractModule() {
            protected void configure() {
              scoper.configure(bind(AWithImplementedBy.class).annotatedWith(named("apple")));
            }
          })
          .creationException("No implementation for %s annotated with %s was bound",
              AWithImplementedBy.class.getName(), named("apple"))
          .addToSuite(suite);

      new Builder()
          .name("bind ScopedA named apple")
          .module(new AbstractModule() {
            protected void configure() {
              scoper.configure(bind(ScopedA.class).annotatedWith(named("apple")));
            }
          })
          .creationException("No implementation for %s annotated with %s was bound",
              ScopedA.class.getName(), named("apple"))
          .addToSuite(suite);

    }
    
    return suite;
  }
  
  enum Scoper {
    UNSCOPED {
      void configure(ScopedBindingBuilder sbb) {}
      void apply(Builder builder) {}
    },

    EAGER_SINGLETON {
      void configure(ScopedBindingBuilder sbb) {
        sbb.asEagerSingleton();
      }
      void apply(Builder builder) {
        builder.expectedValues(new PlainA(101), new PlainA(101), new PlainA(101));
        builder.creationTime(CreationTime.EAGER);
      }
    },

    SCOPES_SINGLETON {
      void configure(ScopedBindingBuilder sbb) {
        sbb.in(Scopes.SINGLETON);
      }
      void apply(Builder builder) {
        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201));
      }
    },

    SINGLETON_DOT_CLASS {
      void configure(ScopedBindingBuilder sbb) {
        sbb.in(Singleton.class);
      }
      void apply(Builder builder) {
        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201));
      }
    },

    TWO_AT_A_TIME_SCOPED_DOT_CLASS {
      void configure(ScopedBindingBuilder sbb) {
        sbb.in(TwoAtATimeScoped.class);
      }
      void apply(Builder builder) {
        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202));
      }
    },

    TWO_AT_A_TIME_SCOPE {
      void configure(ScopedBindingBuilder sbb) {
        sbb.in(new TwoAtATimeScope());
      }
      void apply(Builder builder) {
        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202));
      }
    };

    abstract void configure(ScopedBindingBuilder sbb);
    abstract void apply(Builder builder);
  }

  /** When Guice creates a value, directly or via a provider */
  enum CreationTime {
    NONE, EAGER, LAZY
  }

  public static class Builder {
    private String name = "test";
    private Key<?> key = Key.get(A.class);
    private Class<? extends Injectable> injectsKey = InjectsA.class;
    private List<Module> modules = Lists.<Module>newArrayList(new AbstractModule() {
      protected void configure() {
        bindScope(TwoAtATimeScoped.class, new TwoAtATimeScope());
      }
    });
    private List<Object> expectedValues = Lists.<Object>newArrayList(
        new PlainA(201), new PlainA(202), new PlainA(203));
    private CreationTime creationTime = CreationTime.LAZY;
    private String creationException;
    private String configurationException;

    public Builder module(Module module) {
      this.modules.add(module);
      return this;
    }

    public Builder creationTime(CreationTime creationTime) {
      this.creationTime = creationTime;
      return this;
    }

    public Builder name(String name) {
      this.name = name;
      return this;
    }

    public Builder key(Key<?> key, Class<? extends Injectable> injectsKey) {
      this.key = key;
      this.injectsKey = injectsKey;
      return this;
    }

    private Builder creationException(String message, Object... args) {
      this.creationException = String.format(message, args);
      return this;
    }

    private Builder configurationException(String message, Object... args) {
      configurationException = String.format(message, args);
      return this;
    }

    private Builder scoper(Scoper scoper) {
      name(name + " in " + scoper);
      scoper.apply(this);
      return this;
    }

    private <T> Builder expectedValues(T... values) {
      this.expectedValues.clear();
      Collections.addAll(this.expectedValues, values);
      return this;
    }

    public void addToSuite(TestSuite suite) {
      if (creationException != null) {
        suite.addTest(new CreationExceptionTest(this));

      } else if (configurationException != null) {
        suite.addTest(new ConfigurationExceptionTest(this));

      } else {
        suite.addTest(new SuccessTest(this));
        if (creationTime != CreationTime.NONE) {
          suite.addTest(new UserExceptionsTest(this));
        }
      }
    }
  }

  public static class SuccessTest extends TestCase {
    final String name;
    final Key<?> key;
    final Class<? extends Injectable> injectsKey;
    final ImmutableList<Module> modules;
    final ImmutableList<Object> expectedValues;

    public SuccessTest(Builder builder) {
      super("test");
      name = builder.name;
      key = builder.key;
      injectsKey = builder.injectsKey;
      modules = ImmutableList.copyOf(builder.modules);
      expectedValues = ImmutableList.copyOf(builder.expectedValues);
    }

    public String getName() {
      return name;
    }

    Injector newInjector() {
      nextId.set(101);
      return Guice.createInjector(modules);
    }

    public void test() throws IllegalAccessException, InstantiationException {
      Injector injector = newInjector();
      nextId.set(201);
      for (Object value : expectedValues) {
        assertEquals(value, injector.getInstance(key));
      }

      Provider<?> provider = newInjector().getProvider(key);
      nextId.set(201);
      for (Object value : expectedValues) {
        assertEquals(value, provider.get());
      }

      Provider<?> bindingProvider = newInjector().getBinding(key).getProvider();
      nextId.set(201);
      for (Object value : expectedValues) {
        assertEquals(value, bindingProvider.get());
      }

      injector = newInjector();
      nextId.set(201);
      for (Object value : expectedValues) {
        Injectable instance = injector.getInstance(injectsKey);
        assertEquals(value, instance.value);
      }

      injector = newInjector();
      nextId.set(201);
      for (Object value : expectedValues) {
        Injectable injectable = injectsKey.newInstance();
        injector.injectMembers(injectable);
        assertEquals(value, injectable.value);
      }

      Injector injector1 = newInjector();
      nextId.set(201);
      Injectable hasProvider = injector1.getInstance(injectsKey);
      hasProvider.provider.get();
      nextId.set(201);
      for (Object value : expectedValues) {
        assertEquals(value, hasProvider.provider.get());
      }
    }
  }

  public static class CreationExceptionTest extends TestCase {
    final String name;
    final Key<?> key;
    final ImmutableList<Module> modules;
    final String creationException;

    public CreationExceptionTest(Builder builder) {
      super("test");
      name = builder.name;
      key = builder.key;
      modules = ImmutableList.copyOf(builder.modules);
      creationException = builder.creationException;
    }

    public String getName() {
      return "creation errors:" + name;
    }

    public void test() {
      try {
        Guice.createInjector(modules);
        fail();
      } catch (CreationException expected) {
        assertContains(expected.getMessage(), creationException);
      }
    }
  }

  public static class ConfigurationExceptionTest extends TestCase {
    final String name;
    final Key<?> key;
    final Class<? extends Injectable> injectsKey;
    final ImmutableList<Module> modules;
    final String configurationException;

    public ConfigurationExceptionTest(Builder builder) {
      super("test");
      name = builder.name;
      key = builder.key;
      injectsKey = builder.injectsKey;
      modules = ImmutableList.copyOf(builder.modules);
      configurationException = builder.configurationException;
    }

    public String getName() {
      return "provision errors:" + name;
    }

    Injector newInjector() {
      return Guice.createInjector(modules);
    }

    public void test() throws IllegalAccessException, InstantiationException {
      try {
        newInjector().getProvider(key);
        fail();
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), configurationException);
      }

      try {
        newInjector().getBinding(key).getProvider();
        fail();
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), configurationException);
      }

      try {
        newInjector().getInstance(key);
        fail();
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(), configurationException);
      }

      try {
        newInjector().getInstance(injectsKey);
        fail();
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(),
            configurationException, injectsKey.getName() + ".inject",
            configurationException, injectsKey.getName() + ".inject",
            "2 errors");
      }

      try {
        Injectable injectable = injectsKey.newInstance();
        newInjector().injectMembers(injectable);
        fail();
      } catch (ConfigurationException expected) {
        assertContains(expected.getMessage(),
            configurationException, injectsKey.getName() + ".inject",
            configurationException, injectsKey.getName() + ".inject",
            "2 errors");
      }
    }
  }

  public static class UserExceptionsTest extends TestCase {
    final String name;
    final Key<?> key;
    final Class<? extends Injectable> injectsKey;
    final ImmutableList<Module> modules;
    final ImmutableList<Object> expectedValues;
    final CreationTime creationTime;

    public UserExceptionsTest(Builder builder) {
      super("test");
      name = builder.name;
      key = builder.key;
      injectsKey = builder.injectsKey;
      modules = ImmutableList.copyOf(builder.modules);
      expectedValues = ImmutableList.copyOf(builder.expectedValues);
      creationTime = builder.creationTime;
    }

    public String getName() {
      return "provision errors:" + name;
    }

    Injector newInjector() {
      return Guice.createInjector(modules);
    }

    public void test() throws IllegalAccessException, InstantiationException {
      nextId.set(-1);
      try {
        newInjector();
        assertEquals(CreationTime.LAZY, creationTime);
      } catch (CreationException expected) {
        assertEquals(CreationTime.EAGER, creationTime);
        assertContains(expected.getMessage(), "Illegal value: -1");
        return;
      }

      Provider<?> provider = newInjector().getProvider(key);
      Provider<?> bindingProvider = newInjector().getBinding(key).getProvider();

      nextId.set(-1);
      try {
        newInjector().getInstance(key);
        fail();
      } catch (ProvisionException expected) {
        assertContains(expected.getMessage(), "Illegal value: -1");
      }

      nextId.set(-1);
      try {
        provider.get();
        fail();
      } catch (ProvisionException expected) {
        assertContains(expected.getMessage(), "Illegal value: -1");
      }

      nextId.set(-1);
      try {
        bindingProvider.get();
        fail();
      } catch (ProvisionException expected) {
        assertContains(expected.getMessage(), "Illegal value: -1");
      }

      try {
        nextId.set(-1);
        newInjector().getInstance(injectsKey);
      } catch (ProvisionException expected) {
        assertContains(expected.getMessage(), "Illegal value: -1",
            "for parameter 0 at " + injectsKey.getName() + ".inject");
      }

      nextId.set(201);
      Injectable injectable = injectsKey.newInstance();
      try {
        nextId.set(-1);
        newInjector().injectMembers(injectable);
      } catch (ProvisionException expected) {
        assertContains(expected.getMessage(), "Illegal value: -1",
            "for parameter 0 at " + injectsKey.getName() + ".inject");
      }

      nextId.set(201);
      Injectable hasProvider = newInjector().getInstance(injectsKey);
      hasProvider.provider.get();
      try {
        nextId.set(-1);
        hasProvider.provider.get();
      } catch (ProvisionException expected) {
        assertContains(expected.getMessage(), "Illegal value: -1");
      }
    }
  }

  /** negative to throw, 101... for eager singletons, 201... for everything else */
  static final AtomicInteger nextId = new AtomicInteger();

  @ProvidedBy(PlainAProvider.class)
  interface AWithProvidedBy {}

  static class InjectsAWithProvidedBy extends Injectable {
    @Inject public void inject(AWithProvidedBy aWithProvidedBy,
        Provider<AWithProvidedBy> aWithProvidedByProvider) {
      this.value = aWithProvidedBy;
      this.provider = aWithProvidedByProvider;
    }
  }

  static class InjectsAWithProvidedByNamedApple extends Injectable {
    @Inject public void inject(@Named("apple") AWithProvidedBy aWithProvidedBy,
        @Named("apple") Provider<AWithProvidedBy> aWithProvidedByProvider) {
      this.value = aWithProvidedBy;
      this.provider = aWithProvidedByProvider;
    }
  }

  @ImplementedBy(PlainA.class)
  interface AWithImplementedBy {}

  static class InjectsAWithImplementedBy extends Injectable {
    @Inject public void inject(AWithImplementedBy aWithImplementedBy,
        Provider<AWithImplementedBy> aWithImplementedByProvider) {
      this.value = aWithImplementedBy;
      this.provider = aWithImplementedByProvider;
    }
  }

  static class InjectsAWithImplementedByNamedApple extends Injectable {
    @Inject public void inject(@Named("apple") AWithImplementedBy aWithImplementedBy,
        @Named("apple") Provider<AWithImplementedBy> aWithImplementedByProvider) {
      this.value = aWithImplementedBy;
      this.provider = aWithImplementedByProvider;
    }
  }

  interface A extends AWithProvidedBy, AWithImplementedBy {}

  static class InjectsA extends Injectable {
    @Inject public void inject(A a, Provider<A> aProvider) {
      this.value = a;
      this.provider = aProvider;
    }
  }

  static class PlainA implements A {
    final int value;
    PlainA() {
      value = nextId.getAndIncrement();
      if (value < 0) {
        throw new RuntimeException("Illegal value: " + value);
      }
    }
    PlainA(int value) {
      this.value = value;
    }
    public boolean equals(Object obj) {
      return obj instanceof PlainA
          && value == ((PlainA) obj).value;
    }
    public int hashCode() {
      return value;
    }
    public String toString() {
      return "PlainA#" + value;
    }
  }

  static class PlainAProvider implements Provider<A> {
    public A get() {
      return new PlainA();
    }
  }

  static class InjectsPlainA extends Injectable {
    @Inject public void inject(PlainA plainA, Provider<PlainA> plainAProvider) {
      this.value = plainA;
      this.provider = plainAProvider;
    }
  }

  /** This scope hands out each value exactly twice  */
  static class TwoAtATimeScope implements Scope {
    public <T> Provider<T> scope(Key<T> key, final Provider<T> unscoped) {
      return new Provider<T>() {
        T instance;
        public T get() {
          if (instance == null) {
            instance = unscoped.get();
            return instance;
          } else {
            T result = instance;
            instance = null;
            return result;
          }
        }
      };
    }
  }

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

  @TwoAtATimeScoped
  static class ScopedA extends PlainA {}

  static class InjectsScopedA extends Injectable {
    @Inject public void inject(ScopedA scopedA, Provider<ScopedA> scopedAProvider) {
      this.value = scopedA;
      this.provider = scopedAProvider;
    }
  }

  static class InjectsScopedANamedApple extends Injectable {
    @Inject public void inject(@Named("apple") ScopedA scopedA,
        @Named("apple") Provider<ScopedA> scopedAProvider) {
      this.value = scopedA;
      this.provider = scopedAProvider;
    }
  }

  static class Injectable {
    Object value = new Object();
    Provider<?> provider = Providers.of(new Object());
  }
}
