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

import static com.google.inject.Asserts.asModuleChain;
import static com.google.inject.Asserts.assertContains;
import static com.google.inject.Guice.createInjector;
import static com.google.inject.name.Names.named;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.common.base.Objects;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
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.Provider;
import com.google.inject.Provides;
import com.google.inject.Scope;
import com.google.inject.ScopeAnnotation;
import com.google.inject.Stage;
import com.google.inject.name.Named;
import com.google.inject.util.Modules;

import junit.framework.TestCase;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Date;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author sberlin@gmail.com (Sam Berlin)
 */
public class OverrideModuleTest extends TestCase {

  private static final Key<String> key2 = Key.get(String.class, named("2"));
  private static final Key<String> key3 = Key.get(String.class, named("3"));

  private static final Module EMPTY_MODULE = new Module() {
    public void configure(Binder binder) {}
  };

  public void testOverride() {
    Injector injector = createInjector(Modules.override(newModule("A")).with(newModule("B")));
    assertEquals("B", injector.getInstance(String.class));
  }

  public void testOverrideMultiple() {
    Module module = Modules.override(newModule("A"), newModule(1), newModule(0.5f))
        .with(newModule("B"), newModule(2), newModule(1.5d));
    Injector injector = createInjector(module);
    assertEquals("B", injector.getInstance(String.class));
    assertEquals(2, injector.getInstance(Integer.class).intValue());
    assertEquals(0.5f, injector.getInstance(Float.class));
    assertEquals(1.5d, injector.getInstance(Double.class));
  }

  public void testOverrideUnmatchedTolerated() {
    Injector injector = createInjector(Modules.override(EMPTY_MODULE).with(newModule("B")));
    assertEquals("B", injector.getInstance(String.class));
  }

  public void testOverrideConstant() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bindConstant().annotatedWith(named("Test")).to("A");
      }
    };

    Module replacements = new AbstractModule() {
      @Override protected void configure() {
        bindConstant().annotatedWith(named("Test")).to("B");
      }
    };

    Injector injector = createInjector(Modules.override(original).with(replacements));
    assertEquals("B", injector.getInstance(Key.get(String.class, named("Test"))));
  }

  public void testGetProviderInModule() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("A");
        bind(key2).toProvider(getProvider(String.class));
      }
    };

    Injector injector = createInjector(Modules.override(original).with(EMPTY_MODULE));
    assertEquals("A", injector.getInstance(String.class));
    assertEquals("A", injector.getInstance(key2));
  }

  public void testOverrideWhatGetProviderProvided() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("A");
        bind(key2).toProvider(getProvider(String.class));
      }
    };

    Module replacements = newModule("B");

    Injector injector = createInjector(Modules.override(original).with(replacements));
    assertEquals("B", injector.getInstance(String.class));
    assertEquals("B", injector.getInstance(key2));
  }

  public void testOverrideUsingOriginalsGetProvider() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("A");
        bind(key2).toInstance("B");
      }
    };

    Module replacements = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toProvider(getProvider(key2));
      }
    };

    Injector injector = createInjector(Modules.override(original).with(replacements));
    assertEquals("B", injector.getInstance(String.class));
    assertEquals("B", injector.getInstance(key2));
  }

  public void testOverrideOfOverride() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("A1");
        bind(key2).toInstance("A2");
        bind(key3).toInstance("A3");
      }
    };

    Module replacements1 = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("B1");
        bind(key2).toInstance("B2");
      }
    };

    Module overrides = Modules.override(original).with(replacements1);

    Module replacements2 = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("C1");
        bind(key3).toInstance("C3");
      }
    };

    Injector injector = createInjector(Modules.override(overrides).with(replacements2));
    assertEquals("C1", injector.getInstance(String.class));
    assertEquals("B2", injector.getInstance(key2));
    assertEquals("C3", injector.getInstance(key3));
  }

  static class OuterReplacementsModule extends AbstractModule {
    @Override protected void configure() {
      install(new InnerReplacementsModule());
    }
  }
  static class InnerReplacementsModule extends AbstractModule {
    @Override protected void configure() {
      bind(String.class).toInstance("B");
      bind(String.class).toInstance("C");
    }
  }
  public void testOverridesTwiceFails() {
    Module original = newModule("A");
    Module replacements = new OuterReplacementsModule();
    Module module = Modules.override(original).with(replacements);
    try {
      createInjector(module);
      fail();
    } catch (CreationException expected) {
      assertContains(expected.getMessage(),
          "A binding to java.lang.String was already configured at "
              + InnerReplacementsModule.class.getName(),
          asModuleChain(Modules.OverrideModule.class,
              OuterReplacementsModule.class, InnerReplacementsModule.class),
          "at " + InnerReplacementsModule.class.getName(),
          asModuleChain(Modules.OverrideModule.class,
              OuterReplacementsModule.class, InnerReplacementsModule.class));
    }
  }

  public void testOverridesDoesntFixTwiceBoundInOriginal() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("A");
        bind(String.class).toInstance("B");
      }
    };

    Module replacements = new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).toInstance("C");
      }
    };

    Module module = Modules.override(original).with(replacements);
    try {
      createInjector(module);
      fail();
    } catch (CreationException expected) {
      // The replacement comes first because we replace A with C,
      // then we encounter B and freak out.
      assertContains(expected.getMessage(),
          "1) A binding to java.lang.String was already configured at "
              + replacements.getClass().getName(),
          asModuleChain(Modules.OverrideModule.class, replacements.getClass()),
          "at " + original.getClass().getName(),
          asModuleChain(Modules.OverrideModule.class, original.getClass()));
    }
  }

  public void testStandardScopeAnnotation() {
    final SingleUseScope scope = new SingleUseScope();

    Module module = new AbstractModule() {
      @Override protected void configure() {
        bindScope(TestScopeAnnotation.class, scope);
        bind(String.class).in(TestScopeAnnotation.class);
      }
    };
    assertFalse(scope.used);

    Guice.createInjector(module);
    assertTrue(scope.used);
  }

  public void testOverrideUntargettedBinding() {
    Module original = new AbstractModule() {
      @Override protected void configure() {
        bind(Date.class);
      }
    };

    Module replacements = new AbstractModule() {
      @Override protected void configure() {
        bind(Date.class).toInstance(new Date(0));
      }
    };

    Injector injector = createInjector(Modules.override(original).with(replacements));
    assertEquals(0, injector.getInstance(Date.class).getTime());
  }

  public void testOverrideScopeAnnotation() {
    final Scope scope = new Scope() {
      public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
        throw new AssertionError("Should not be called");
      }
    };

    final SingleUseScope replacementScope = new SingleUseScope();

    Module original = new AbstractModule() {
      @Override protected void configure() {
        bindScope(TestScopeAnnotation.class, scope);
        bind(Date.class).in(TestScopeAnnotation.class);
      }
    };

    Module replacements = new AbstractModule() {
      @Override protected void configure() {
        bindScope(TestScopeAnnotation.class, replacementScope);
      }
    };

    Injector injector = createInjector(Modules.override(original).with(replacements));
    injector.getInstance(Date.class);
    assertTrue(replacementScope.used);
  }

  public void testFailsIfOverridenScopeInstanceHasBeenUsed() {
    final Scope scope = new Scope() {
      public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
        return unscoped;
      }

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

    final Module original = new AbstractModule() {
      @Override protected void configure() {
        bindScope(TestScopeAnnotation.class, scope);
        bind(Date.class).in(scope);
        bind(String.class).in(scope);
      }
    };
    Module originalWrapper = new AbstractModule() {
      @Override protected void configure() {
        install(original);
      }
    };

    Module replacements = new AbstractModule() {
      @Override protected void configure() {
        bindScope(TestScopeAnnotation.class, new SingleUseScope());
      }
    };

    try {
      createInjector(Modules.override(originalWrapper).with(replacements));
      fail("Exception expected");
    } catch (CreationException e) {
      assertContains(e.getMessage(),
          "1) The scope for @TestScopeAnnotation is bound directly and cannot be overridden.",
          "original binding at " + original.getClass().getName() + ".configure(",
          asModuleChain(originalWrapper.getClass(), original.getClass()),
          "bound directly at " + original.getClass().getName() + ".configure(",
          asModuleChain(originalWrapper.getClass(), original.getClass()),
          "bound directly at " + original.getClass().getName() + ".configure(",
          asModuleChain(originalWrapper.getClass(), original.getClass()),          
          "at ", replacements.getClass().getName() + ".configure(",
          asModuleChain(Modules.OverrideModule.class, replacements.getClass()));
    }
  }

  public void testOverrideIsLazy() {
    final AtomicReference<String> value = new AtomicReference<String>("A");
    Module overridden = Modules.override(new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).annotatedWith(named("original")).toInstance(value.get());
      }
    }).with(new AbstractModule() {
      @Override protected void configure() {
        bind(String.class).annotatedWith(named("override")).toInstance(value.get());
      }
    });

    // the value.get() call should be deferred until Guice.createInjector
    value.set("B");
    Injector injector = Guice.createInjector(overridden);
    assertEquals("B", injector.getInstance(Key.get(String.class, named("original"))));
    assertEquals("B", injector.getInstance(Key.get(String.class, named("override"))));
  }

  public void testOverridePrivateModuleOverPrivateModule() {
    Module exposes5and6 = new AbstractModule() {
      @Override protected void configure() {
        install(new PrivateModule() {
          @Override protected void configure() {
            bind(Integer.class).toInstance(5);
            expose(Integer.class);

            bind(Character.class).toInstance('E');
          }
        });

        install(new PrivateModule() {
          @Override protected void configure() {
            bind(Long.class).toInstance(6L);
            expose(Long.class);

            bind(Character.class).toInstance('F');
          }
        });
      }
    };

    AbstractModule exposes15 = new AbstractModule() {
      @Override protected void configure() {
        install(new PrivateModule() {
          @Override protected void configure() {
            bind(Integer.class).toInstance(15);
            expose(Integer.class);

            bind(Character.class).toInstance('G');
          }
        });

        install(new PrivateModule() {
          @Override protected void configure() {
            bind(Character.class).toInstance('H');
          }
        });
      }
    };

    // override forwards
    Injector injector = Guice.createInjector(Modules.override(exposes5and6).with(exposes15));
    assertEquals(15, injector.getInstance(Integer.class).intValue());
    assertEquals(6L, injector.getInstance(Long.class).longValue());

    // and in reverse order
    Injector reverse = Guice.createInjector(Modules.override(exposes15).with(exposes5and6));
    assertEquals(5, reverse.getInstance(Integer.class).intValue());
    assertEquals(6L, reverse.getInstance(Long.class).longValue());
  }

  public void testOverrideModuleAndPrivateModule() {
    Module exposes5 = new PrivateModule() {
      @Override protected void configure() {
        bind(Integer.class).toInstance(5);
        expose(Integer.class);
      }
    };

    Module binds15 = new AbstractModule() {
      @Override protected void configure() {
        bind(Integer.class).toInstance(15);
      }
    };

    Injector injector = Guice.createInjector(Modules.override(exposes5).with(binds15));
    assertEquals(15, injector.getInstance(Integer.class).intValue());

    Injector reverse = Guice.createInjector(Modules.override(binds15).with(exposes5));
    assertEquals(5, reverse.getInstance(Integer.class).intValue());
  }

  public void testOverrideDeepExpose() {
    final AtomicReference<Provider<Character>> charAProvider
        = new AtomicReference<Provider<Character>>();

    Module exposes5 = new PrivateModule() {
      @Override protected void configure() {
        install(new PrivateModule() {
          @Override protected void configure() {
            bind(Integer.class).toInstance(5);
            expose(Integer.class);
            charAProvider.set(getProvider(Character.class));
            bind(Character.class).toInstance('A');
          }
        });
        expose(Integer.class);
      }
    };

    Injector injector = Guice.createInjector(Modules.override(exposes5).with(EMPTY_MODULE));
    assertEquals(5, injector.getInstance(Integer.class).intValue());
    assertEquals('A', charAProvider.getAndSet(null).get().charValue());

    injector = Guice.createInjector(Modules.override(EMPTY_MODULE).with(exposes5));
    assertEquals(5, injector.getInstance(Integer.class).intValue());
    assertEquals('A', charAProvider.getAndSet(null).get().charValue());

    final AtomicReference<Provider<Character>> charBProvider
        = new AtomicReference<Provider<Character>>();

    Module binds15 = new AbstractModule() {
      @Override protected void configure() {
        bind(Integer.class).toInstance(15);

        install(new PrivateModule() {
          @Override protected void configure() {
            charBProvider.set(getProvider(Character.class));
            bind(Character.class).toInstance('B');
          }
        });
      }
    };

    injector = Guice.createInjector(Modules.override(binds15).with(exposes5));
    assertEquals(5, injector.getInstance(Integer.class).intValue());
    assertEquals('A', charAProvider.getAndSet(null).get().charValue());
    assertEquals('B', charBProvider.getAndSet(null).get().charValue());

    injector = Guice.createInjector(Modules.override(exposes5).with(binds15));
    assertEquals(15, injector.getInstance(Integer.class).intValue());
    assertEquals('A', charAProvider.getAndSet(null).get().charValue());
    assertEquals('B', charBProvider.getAndSet(null).get().charValue());
  }

  @Retention(RUNTIME)
  @Target(TYPE)
  @ScopeAnnotation
  private static @interface TestScopeAnnotation {}

  private static class SingleUseScope implements Scope {
    boolean used = false;
    public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
      assertFalse(used);
      used = true;
      return unscoped;
    }
  }
  
  static class NewModule<T> extends AbstractModule {
    private final T bound;
    NewModule(T bound) {
      this.bound = bound;
    }
    @Override protected void configure() {
      @SuppressWarnings("unchecked")
      Class<T> type = (Class<T>)bound.getClass();
      bind(type).toInstance(bound);
    }
  }

  private static <T> Module newModule(final T bound) {
    return new NewModule<T>(bound);
  }
  
  private static final String RESULT = "RESULT";
  private static final String PRIVATE_INPUT = "PRIVATE_INPUT";
  private static final String OVERRIDDEN_INPUT = "FOO";
  private static final String OVERRIDDEN_RESULT = "Size: 3";
  private static final Key<String> RESULT_KEY = Key.get(String.class, named(RESULT));
  private static final Key<String> INPUT_KEY = Key.get(String.class, named(PRIVATE_INPUT));

  public void testExposedBindingOverride() throws Exception {
    Injector inj = Guice.createInjector(
        Modules.override(new ExampleModule()).with(
            new AbstractModule() {
              @Override protected void configure() {
                bind(RESULT_KEY).toInstance(OVERRIDDEN_RESULT);
              }
            }));
    assertEquals(inj.getInstance(RESULT_KEY), OVERRIDDEN_RESULT);
  }

  public void testPrivateBindingOverride() throws Exception {
    Injector inj = Guice.createInjector(
        Modules.override(new ExampleModule()).with(
            new AbstractModule() {
              @Override protected void configure() {
                bind(INPUT_KEY).toInstance(OVERRIDDEN_INPUT);
              }
            }));
    assertEquals(inj.getInstance(RESULT_KEY), OVERRIDDEN_RESULT);
  }

  public static class ExampleModule extends PrivateModule {
    @Provides @Exposed @Named(RESULT)
    public String provideResult(@Named(PRIVATE_INPUT) String input) {
      return "Size: " + input.length();
    }

    @Provides @Named(PRIVATE_INPUT)
    public String provideInput() {
      return "Hello World";
    }

    @Override protected void configure() {
    }
  }  
  
  public void testEqualsNotCalledByDefaultOnInstance() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnEquals = true;
    Guice.createInjector(Modules.override(new AbstractModule() {
      @Override
      protected void configure() {
       bind(String.class);
       bind(HashEqualsTester.class).toInstance(a);
      }
    }).with());
  }
  
  public void testEqualsNotCalledByDefaultOnProvider() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnEquals = true;
    Guice.createInjector(Modules.override(new AbstractModule() {
      @Override
      protected void configure() {
       bind(String.class);
       bind(Object.class).toProvider(a);
      }
    }).with());
  }
  
  public void testHashcodeNeverCalledOnInstance() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnHashcode = true;
    a.equality = "test";
    
    final HashEqualsTester b = new HashEqualsTester();
    b.throwOnHashcode = true;
    b.equality = "test";
    Guice.createInjector(Modules.override(new AbstractModule() {
      @Override
      protected void configure() {
       bind(String.class);
       bind(HashEqualsTester.class).toInstance(a);
       bind(HashEqualsTester.class).toInstance(b);
      }
    }).with());
  }
  
  public void testHashcodeNeverCalledOnProviderInstance() {
    final HashEqualsTester a = new HashEqualsTester();
    a.throwOnHashcode = true;
    a.equality = "test";
    
    final HashEqualsTester b = new HashEqualsTester();
    b.throwOnHashcode = true;
    b.equality = "test";
    Guice.createInjector(Modules.override(new AbstractModule() {
      @Override
      protected void configure() {
       bind(String.class);
       bind(Object.class).toProvider(a);
       bind(Object.class).toProvider(b);
      }
    }).with());
  }
  
  private static class HashEqualsTester implements Provider<Object> {
    private String equality;
    private boolean throwOnEquals;
    private boolean throwOnHashcode;
    
    @Override
    public boolean equals(Object obj) {
      if (throwOnEquals) {
        throw new RuntimeException();
      } else if (obj instanceof HashEqualsTester) {
        HashEqualsTester o = (HashEqualsTester)obj;
        if(o.throwOnEquals) {
          throw new RuntimeException();
        }
        if(equality == null && o.equality == null) {
          return this == o;
        } else {
          return Objects.equal(equality, o.equality);
        }
      } else {
        return false;
      }
    }
    
    @Override
    public int hashCode() {
      if(throwOnHashcode) {
        throw new RuntimeException();
      } else {
        return super.hashCode();
      }
    }
    
    public Object get() {
      return new Object();
    }
  }

  public void testCorrectStage() {
    final Stage stage = Stage.PRODUCTION;
    Module module = Modules.override(new AbstractModule() {
      @Override
      protected void configure() {
        if (currentStage() != Stage.PRODUCTION) {
          addError("Wronge stage in overridden module:" + currentStage());
        }
      }
    }).with(new AbstractModule() {
      @Override
      protected void configure() {
        if (currentStage() != Stage.PRODUCTION) {
          addError("Wronge stage in overriding module:" + currentStage());
        }
      }
    });
    Guice.createInjector(stage, module);
  }
}
