/*
 * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.ioc.tests.wiring.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.enterprise.inject.Any;
import javax.inject.Named;

import org.jboss.errai.ioc.client.JsArray;
import org.jboss.errai.ioc.client.WindowInjectionContext;
import org.jboss.errai.ioc.client.WindowInjectionContextStorage;
import org.jboss.errai.ioc.client.container.DynamicAnnotation;
import org.jboss.errai.ioc.client.container.Factory;
import org.jboss.errai.ioc.client.container.IOC;
import org.jboss.errai.ioc.client.container.IOCResolutionException;
import org.jboss.errai.ioc.client.container.JsTypeProvider;
import org.jboss.errai.ioc.client.container.Proxy;
import org.jboss.errai.ioc.client.container.SyncBeanDef;
import org.jboss.errai.ioc.client.container.SyncBeanManagerImpl;
import org.jboss.errai.ioc.client.test.AbstractErraiIOCTest;
import org.jboss.errai.ioc.tests.wiring.client.res.AlternativeImpl;
import org.jboss.errai.ioc.tests.wiring.client.res.ConcreteWindowScopedJsType;
import org.jboss.errai.ioc.tests.wiring.client.res.ConsumesProducedJsType;
import org.jboss.errai.ioc.tests.wiring.client.res.DuplicateInterface;
import org.jboss.errai.ioc.tests.wiring.client.res.ExternalJsTypeIface;
import org.jboss.errai.ioc.tests.wiring.client.res.ExternalJsTypeImpl;
import org.jboss.errai.ioc.tests.wiring.client.res.ExternalTestModule;
import org.jboss.errai.ioc.tests.wiring.client.res.InternallySatisfiedImpl;
import org.jboss.errai.ioc.tests.wiring.client.res.InternallySatisfiedJsTypeIface;
import org.jboss.errai.ioc.tests.wiring.client.res.InternallyUnsatisfiedJsTypeIface;
import org.jboss.errai.ioc.tests.wiring.client.res.JsSubTypeWithDuplicateInterface;
import org.jboss.errai.ioc.tests.wiring.client.res.JsSuperTypeWithDuplicateInterface;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeConsumer;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeDependentBean;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeDependentInterface;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeNamedBean;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeSingletonBean;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeSingletonInterface;
import org.jboss.errai.ioc.tests.wiring.client.res.JsTypeWithQualifiers;
import org.jboss.errai.ioc.tests.wiring.client.res.MultipleImplementationsJsType;
import org.jboss.errai.ioc.tests.wiring.client.res.NativeConcreteJsType;
import org.jboss.errai.ioc.tests.wiring.client.res.NativeConcreteJsTypeWithConstructorDependency;
import org.jboss.errai.ioc.tests.wiring.client.res.NativeConcreteJsTypeWithFieldDependency;
import org.jboss.errai.ioc.tests.wiring.client.res.NativeTypeTestModule;
import org.jboss.errai.ioc.tests.wiring.client.res.ProducedJsType;
import org.jboss.errai.ioc.tests.wiring.client.res.QualWithMultiMembers;
import org.jboss.errai.ioc.tests.wiring.client.res.UnimplementedType;

import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.ScriptInjector;
import com.google.gwt.user.client.Timer;

public class JsTypeInjectionTest extends AbstractErraiIOCTest {

  @Override
  public String getModuleName() {
    return "org.jboss.errai.ioc.tests.wiring.IOCWiringTests";
  }

  @Override
  protected void gwtSetUp() throws Exception {
    WindowInjectionContextStorage.reset();
    final WindowInjectionContext windowInjContext = WindowInjectionContextStorage.createOrGet();
    windowInjContext.addBeanProvider(MultipleImplementationsJsType.class.getName(), new JsTypeProvider<Object>() {
      @Override
      public Object getInstance() {
        return new AlternativeImpl(1);
      }

      @Override
      public String getName() {
        return null;
      }

      @Override
      public String getFactoryName() {
        return null;
      }

      @Override
      public JsArray<String> getQualifiers() {
        return new JsArray<String>(new String[0]);
      }
    });
    windowInjContext.addBeanProvider(MultipleImplementationsJsType.class.getName(), new JsTypeProvider<Object>() {
      @Override
      public Object getInstance() {
        return new AlternativeImpl(2);
      }

      @Override
      public String getName() {
        return null;
      }

      @Override
      public String getFactoryName() {
        return null;
      }

      @Override
      public JsArray<String> getQualifiers() {
        return new JsArray<String>(new String[0]);
      }
    });
    windowInjContext.addBeanProvider(InternallyUnsatisfiedJsTypeIface.class.getName(),
            new JsTypeProvider<InternallyUnsatisfiedJsTypeIface>() {

      @Override
      public InternallyUnsatisfiedJsTypeIface getInstance() {
        return new InternallyUnsatisfiedJsTypeIface() {
          @Override
          public String message() {
            return "external";
          }
        };
      }

      @Override
      public String getName() {
        return null;
      }

      @Override
      public String getFactoryName() {
        return null;
      }

      @Override
      public JsArray<String> getQualifiers() {
        return new JsArray<String>(new String[0]);
      }
    });
    windowInjContext.addBeanProvider(ExternalJsTypeImpl.class.getName(),
            new JsTypeProvider<ExternalJsTypeIface>() {

              @Override
              public ExternalJsTypeIface getInstance() {
                return new ExternalJsTypeIface() {

                  @Override
                  public String message() {
                    return "external";
                  }
                };
              }

              @Override
              public String getName() {
                return null;
              }

              @Override
              public String getFactoryName() {
                return null;
              }

              @Override
              public JsArray<String> getQualifiers() {
                return new JsArray<String>(new String[0]);
              }
    });
    windowInjContext.addBeanProvider(ConcreteWindowScopedJsType.class.getName(),
            new JsTypeProvider<ConcreteWindowScopedJsType>() {

      @Override
      public ConcreteWindowScopedJsType getInstance() {
        return new ConcreteWindowScopedJsType() {
          @Override
          public String message() {
            return "external";
          }
        };
      }

      @Override
      public String getName() {
        return null;
      }

      @Override
      public String getFactoryName() {
        return null;
      }

      @Override
      public JsArray<String> getQualifiers() {
        return new JsArray<String>(new String[0]);
      }
    });
    windowInjContext.addSuperTypeAlias(ExternalJsTypeIface.class.getName(), ExternalJsTypeImpl.class.getName());
    super.gwtSetUp();
  }

  public void testSingletonJsTypeInWindowContext() {
    final WindowInjectionContext wndContext = WindowInjectionContextStorage.createOrGet();

    final Object bean1 = wndContext.getBean(JsTypeSingletonBean.class.getName());
    assertNotNull("@JsType bean was not registered in window context", bean1);

    final Object bean2 = wndContext.getBean(JsTypeSingletonInterface.class.getName());
    assertNotNull("@JsType bean was not registered using its interface", bean2);

    assertSame(bean1, bean2);
  }

  public void testDependentJsTypeInWindowContext() {
    final WindowInjectionContext wndContext = WindowInjectionContextStorage.createOrGet();

    final Object bean1 = wndContext.getBean(JsTypeDependentBean.class.getName());
    assertNotNull("@JsType bean was not registered in window context", bean1);

    final Object bean2 = wndContext.getBean(JsTypeDependentInterface.class.getName());
    assertNotNull("@JsType bean was not registered using its interface", bean2);

    assertNotSame(bean1, bean2);
  }

  @SuppressWarnings("rawtypes")
  public void testNamedJsTypeInWindowContext() {
    final WindowInjectionContext wndContext = WindowInjectionContextStorage.createOrGet();

    final Object bean1 = wndContext.getBean(JsTypeNamedBean.class.getName());
    assertNotNull("@JsType bean was not registered in window context", bean1);

    final Object bean2 = wndContext.getBean("olaf");
    assertNotNull("@JsType bean was not registered using its interface", bean2);
    assertSame(bean1, bean2);

    final Collection<SyncBeanDef> beans = new ArrayList<>(IOC.getBeanManager().lookupBeans("olaf"));
    beans.addAll(IOC.getBeanManager().lookupBeans(JsTypeNamedBean.class.getName()));
    for (final SyncBeanDef bean : beans) {
      assertEquals("olaf", bean.getName());
    }
  }

  @SuppressWarnings("rawtypes")
  public void testNoDuplicateJsTypeThroughBeanManager() {
    final WindowInjectionContext wndContext = WindowInjectionContextStorage.createOrGet();

    final Object bean1 = wndContext.getBean(JsTypeNamedBean.class.getName());
    assertNotNull("@JsType bean was not registered in window context", bean1);

    final Collection<SyncBeanDef> beans = IOC.getBeanManager().lookupBeans(JsTypeNamedBean.class.getName());
    assertEquals(1, beans.size());
    assertEquals("olaf", beans.iterator().next().getName());
    assertSame(bean1, beans.iterator().next().getInstance());
  }

  public void testConsumingOfUnimplementedJsType() throws Exception {
    injectScriptThenRun(() -> {
      final UnimplementedType ref = new UnimplementedType() {

        @Override
        public void overloaded(final Object obj) {
        }

        @Override
        protected void overloaded() {
        }
      };

      final WindowInjectionContext wndContext = WindowInjectionContextStorage.createOrGet();
      wndContext.addBeanProvider("org.jboss.errai.ioc.tests.wiring.client.res.UnimplementedType", new JsTypeProvider<Object>(){

        @Override
        public Object getInstance() {
          return ref;
        }

        @Override
        public String getName() {
          return null;
        }

        @Override
        public String getFactoryName() {
          return null;
        }

        @Override
        public JsArray<String> getQualifiers() {
          return new JsArray<String>(new String[0]);
        }
      });

      final SyncBeanDef<JsTypeConsumer> consumer = IOC.getBeanManager().lookupBean(JsTypeConsumer.class);

      assertSame(ref, Factory.maybeUnwrapProxy(consumer.getInstance().getIface()));
    });
  }

  public void testNativeJsTypesNotInWindowContext() throws Exception {
    injectScriptThenRun(() -> {
      final WindowInjectionContext context = WindowInjectionContextStorage.createOrGet();
      try {
        context.getBean("org.jboss.errai.ioc.tests.wiring.client.res.NativeConcreteJsType");
        fail("There should not be a provider in the WindowInjectionContext for NativeConcreteJsType.");
      } catch (final IOCResolutionException ex) {
      }
    });
  }

  public void testNoArgInstantiableNativeJsTypeIsInjectable() throws Exception {
    injectScriptThenRun(() -> {
      try {
        final NativeTypeTestModule module = IOC.getBeanManager().lookupBean(NativeTypeTestModule.class).getInstance();
        final NativeConcreteJsType instance = module.nativeConcreteJsType;
        assertEquals("Not the expected implementation (in native.js).", "I am a native type!", instance.message());
      } catch (final IOCResolutionException ex) {
        fail("Precondition failed: Problem looking up test module.");
      }
    });
  }

  public void testInstantiableNativeJsTypeWithConstructorDependencyIsInjectable() throws Exception {
    injectScriptThenRun(() -> {
      try {
        final NativeTypeTestModule module = IOC.getBeanManager().lookupBean(NativeTypeTestModule.class).getInstance();
        final NativeConcreteJsTypeWithConstructorDependency instance = module.nativeWithConstructorDep;
        assertNotNull(instance.get());
        assertEquals("Not the expected implementation (in native.js).", "I am a native type!", instance.get().message());
      } catch (final IOCResolutionException ex) {
        fail("Precondition failed: Problem looking up test module.");
      }
    });
  }

  public void testInstantiableNativeJsTypeWithFieldDependencyIsInjectable() throws Exception {
    injectScriptThenRun(() -> {
      try {
        final NativeTypeTestModule module = IOC.getBeanManager().lookupBean(NativeTypeTestModule.class).getInstance();
        final NativeConcreteJsTypeWithFieldDependency instance = module.nativeWithFieldDep;
        assertNotNull(instance.get());
        assertEquals("Not the expected implementation (in native.js).", "I am a native type!", instance.get().message());
      } catch (final IOCResolutionException ex) {
        fail("Precondition failed: Problem looking up test module.");
      }
    });
  }

  public void testProducerMethodNativeOfJsType() throws Exception {
    injectScriptThenRun(() -> {
      try {
        final NativeTypeTestModule module = IOC.getBeanManager().lookupBean(NativeTypeTestModule.class).getInstance();
        assertNotNull(module.producedNativeIface);
        assertEquals("Not the expected implementation (in native.js).", "please", module.producedNativeIface.getMagicWord());
      } catch (final IOCResolutionException ex) {
        fail("Precondition failed: Problem looking up test module.");
      }
    });
  }

  public void testProducerMethodOfJsType() throws Exception {
    final ConsumesProducedJsType consumer = IOC.getBeanManager().lookupBean(ConsumesProducedJsType.class).getInstance();
    assertNotNull(consumer.instance);
    assertTrue(consumer.instance instanceof ProducedJsType);
    assertEquals(1, IOC.getBeanManager().lookupBeans(ProducedJsType.class).size());
  }

  public void testMultipleJsTypeImplementations() throws Exception {
    Collection<SyncBeanDef<MultipleImplementationsJsType>> beans = IOC.getBeanManager().lookupBeans(MultipleImplementationsJsType.class);
    assertEquals(2, beans.size());

    final Set<Integer> values = new HashSet<>();
    for (final SyncBeanDef<MultipleImplementationsJsType> bean : beans) {
      final MultipleImplementationsJsType instance = bean.getInstance();
      assertTrue(instance instanceof AlternativeImpl);
      values.add(instance.value());
    }

    assertEquals(new HashSet<>(Arrays.asList(1, 2)), values);

    WindowInjectionContextStorage.createOrGet().addBeanProvider(MultipleImplementationsJsType.class.getName(), new JsTypeProvider<Object>() {
      @Override
      public Object getInstance() {
        return new AlternativeImpl(3);
      }

      @Override
      public String getName() {
        return null;
      }

      @Override
      public String getFactoryName() {
        return null;
      }

      @Override
      public JsArray<String> getQualifiers() {
        return new JsArray<String>(new String[0]);
      }
    });
    values.clear();
    beans = IOC.getBeanManager().lookupBeans(MultipleImplementationsJsType.class);
    for (final SyncBeanDef<MultipleImplementationsJsType> bean : beans) {
      values.add(bean.getInstance().value());
    }
    assertEquals(new HashSet<>(Arrays.asList(1, 2, 3)), values);
  }

  public void testNoDuplicatesWithSuperTypeAliases() throws Exception {
    final JsArray<JsTypeProvider<?>> providers = WindowInjectionContextStorage.createOrGet()
            .getProviders(DuplicateInterface.class.getName());

    assertEquals(2, providers.length());
    assertEquals(
            new HashSet<>(Arrays.asList(
                    JsSubTypeWithDuplicateInterface.class.getName(),
                    JsSuperTypeWithDuplicateInterface.class.getName())),
            new HashSet<>(Arrays.asList(
                    providers.get(0).getInstance().getClass().getName(),
                    providers.get(1).getInstance().getClass().getName())));
  }

  public void testLocalBeanSatisfiesJsTypeInterfaceForDefaultInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.defaultSatisfiedIface);
    assertTrue(module.defaultSatisfiedIface instanceof InternallySatisfiedImpl);
    assertSame(module.defaultSatisfiedIface, IOC.getBeanManager().lookupBean(InternallySatisfiedJsTypeIface.class).getInstance());
  }

  public void testLocalBeanSatisfiesJsTypeInterfaceForExternalInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.externalSatisfiedIface);
    assertTrue(module.externalSatisfiedIface instanceof InternallySatisfiedImpl);
    assertSame(module.externalSatisfiedIface,
            IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance().externalSatisfiedIface);
  }

  public void testWindowContextBeanSatisfiesJsTypeInterfaceForDefaultInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.defaultUnsatisfiedIface);
    assertEquals("external", module.defaultUnsatisfiedIface.message());
  }

  public void testWindowContextBeanSatisfiesJsTypeInterfaceForExternalInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.defaultUnsatisfiedIface);
    assertEquals("external", module.defaultUnsatisfiedIface.message());
  }

  public void testImplNotPublishedUnderExternalJsTypeIfaceWhenAlreadyInWindowContext() throws Exception {
    assertEquals(1, WindowInjectionContextStorage.createOrGet().getProviders(ExternalJsTypeIface.class.getName()).length());
  }

  public void testLocalBeanSatisfiesExternalJsTypeInterfaceForDefaultInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.defaultExternalIface);
    assertEquals(ExternalJsTypeImpl.class.getSimpleName(), module.defaultExternalIface.message());
    assertTrue(module.defaultExternalIface instanceof ExternalJsTypeImpl);
  }

  public void testWindowContextBeanSatisfiesExternalJsTypeInterfaceForExternalInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.externalExternalIface);
    assertEquals("external", module.externalExternalIface.message());
    assertFalse(module.externalExternalIface instanceof ExternalJsTypeImpl);
  }

  public void testWindowContextBeanSatisfiesConcreteWindowScopedJsTypeForDefaultInjectionSite() throws Exception {
    final ExternalTestModule module = IOC.getBeanManager().lookupBean(ExternalTestModule.class).getInstance();
    assertNotNull(module.defaultConcreteWindowScopedJsType);
    assertEquals("external", module.defaultConcreteWindowScopedJsType.message());
  }

  public void testQualifiersOnJsType() throws Exception {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    final Collection<SyncBeanDef<JsTypeWithQualifiers>> beans = (Collection) ((SyncBeanManagerImpl) IOC.getBeanManager())
            .lookupBeans(JsTypeWithQualifiers.class.getName(), true);
    SyncBeanDef<JsTypeWithQualifiers> beanDef = null;
    for (final SyncBeanDef<JsTypeWithQualifiers> bd : beans) {
      if (bd.isDynamic()) {
        beanDef = bd;
      }
    }
    assertNotNull("No bean JS bean def found", beanDef);

    @SuppressWarnings({ "unchecked", "rawtypes" })
    final Set<DynamicAnnotation> quals = (Set) beanDef.getQualifiers();
    assertEquals(3, quals.size());

    final Set<String> notYetFound = new HashSet<>(Arrays.asList(Named.class.getName(),
            QualWithMultiMembers.class.getName(), Any.class.getName()));
    for (final DynamicAnnotation qual : quals) {
      final Map<String, String> members = qual.getMembers();
      if (Any.class.getName().equals(qual.getName())) {
        assertEquals(0, members.size());
        notYetFound.remove(Any.class.getName());
      }
      else if (Named.class.getName().equals(qual.getName())) {
        assertEquals(1, members.size());
        assertEquals("Moogah", members.get("value"));
        notYetFound.remove(Named.class.getName());
      }
      else if (QualWithMultiMembers.class.getName().equals(qual.getName())) {
        assertEquals(3, members.size());
        assertEquals("1", members.get("num"));
        assertEquals("foo", members.get("text"));
        assertEquals(Arrays.toString(new String[] {JsTypeWithQualifiers.class.getName()}), members.get("clazzes"));
        notYetFound.remove(QualWithMultiMembers.class.getName());
      }
    }

    assertEquals(0, notYetFound.size());
  }

  public void testSingletonJsTypeIsNotProxied() throws Exception {
    final NativeTypeTestModule module = IOC.getBeanManager().lookupBean(NativeTypeTestModule.class).getInstance();
    assertEquals("Sanity check for correct bean failed.", "please", module.singletonJsType.magicWord());
    assertFalse("Singleton JS bean should not be proxied.", module.singletonJsType instanceof Proxy);
  }

  private void injectScriptThenRun(final Runnable test) {
    final String scriptUrl = getScriptUrl();
    final Timer timeoutFail = new Timer() {

      @Override
      public void run() {
        fail("Timed out waiting to load " + scriptUrl);
      }
    };

    timeoutFail.schedule(9000);
    delayTestFinish(10000);
    ScriptInjector.fromUrl(scriptUrl)
                  .setWindow(ScriptInjector.TOP_WINDOW)
                  .setCallback(new Callback<Void, Exception>() {

                    @Override
                    public void onFailure(final Exception reason) {
                      timeoutFail.cancel();
                      fail("Could not load " + scriptUrl);
                    }

                    @Override
                    public void onSuccess(final Void result) {
                      try {
                        test.run();
                        finishTest();
                      } finally {
                        timeoutFail.cancel();
                      }
                    }
                  }).inject();
  }

  private String getScriptUrl() {
    final String scriptUrl = GWT.getModuleBaseForStaticFiles() + "native.js";
    return scriptUrl;
  }
}
