/*
 * 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.cdi.injection.client.test;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Any;

import org.jboss.errai.cdi.injection.client.AbstractBean;
import org.jboss.errai.cdi.injection.client.ApplicationScopedBean;
import org.jboss.errai.cdi.injection.client.CommonInterface;
import org.jboss.errai.cdi.injection.client.CommonInterfaceB;
import org.jboss.errai.cdi.injection.client.Cow;
import org.jboss.errai.cdi.injection.client.CreditCard;
import org.jboss.errai.cdi.injection.client.DependentScopedBean;
import org.jboss.errai.cdi.injection.client.DependentScopedBeanWithDependencies;
import org.jboss.errai.cdi.injection.client.DisabledAlternativeBean;
import org.jboss.errai.cdi.injection.client.FoobieScopedBean;
import org.jboss.errai.cdi.injection.client.FoobieScopedOverriddenBean;
import org.jboss.errai.cdi.injection.client.InheritedApplicationScopedBean;
import org.jboss.errai.cdi.injection.client.InheritedFromAbstractBean;
import org.jboss.errai.cdi.injection.client.InterfaceA;
import org.jboss.errai.cdi.injection.client.InterfaceB;
import org.jboss.errai.cdi.injection.client.InterfaceC;
import org.jboss.errai.cdi.injection.client.InterfaceD;
import org.jboss.errai.cdi.injection.client.InterfaceRoot;
import org.jboss.errai.cdi.injection.client.InterfaceWithNamedImpls;
import org.jboss.errai.cdi.injection.client.OuterBeanInterface;
import org.jboss.errai.cdi.injection.client.Pig;
import org.jboss.errai.cdi.injection.client.Visa;
import org.jboss.errai.cdi.injection.client.qualifier.LincolnBar;
import org.jboss.errai.cdi.injection.client.qualifier.QualA;
import org.jboss.errai.cdi.injection.client.qualifier.QualAppScopeBeanA;
import org.jboss.errai.cdi.injection.client.qualifier.QualAppScopeBeanB;
import org.jboss.errai.cdi.injection.client.qualifier.QualB;
import org.jboss.errai.cdi.injection.client.qualifier.QualEnum;
import org.jboss.errai.cdi.injection.client.qualifier.QualParmAppScopeBeanApples;
import org.jboss.errai.cdi.injection.client.qualifier.QualParmAppScopeBeanOranges;
import org.jboss.errai.cdi.injection.client.qualifier.QualV;
import org.jboss.errai.enterprise.client.cdi.AbstractErraiCDITest;
import org.jboss.errai.ioc.client.container.DestructionCallback;
import org.jboss.errai.ioc.client.container.IOC;
import org.jboss.errai.ioc.client.container.IOCResolutionException;
import org.jboss.errai.ioc.client.container.SyncBeanDef;
import org.jboss.errai.ioc.client.container.SyncBeanManager;

/**
 * @author Mike Brock
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public class BeanManagerIntegrationTest extends AbstractErraiCDITest {
  {
    disableBus = true;
  }

  private final QualA QUAL_A = new QualA() {
    @Override
    public Class<? extends Annotation> annotationType() {
      return QualA.class;
    }
  };

  private final Any anyAnno = new Any() {

    @Override
    public Class<? extends Annotation> annotationType() {
      return Any.class;
    }
  };

  @Override
  public String getModuleName() {
    return "org.jboss.errai.cdi.injection.InjectionTestModule";
  }

  public void testBeanManagerLookupSimple() {
    final SyncBeanDef<ApplicationScopedBean> bean = IOC.getBeanManager().lookupBean(ApplicationScopedBean.class);
    assertNotNull(bean);
  }

  public void testBeanManagerLookupInheritedScopeBean() {
    final SyncBeanDef<InheritedApplicationScopedBean> bean =
        IOC.getBeanManager().lookupBean(InheritedApplicationScopedBean.class, anyAnno);
    assertNotNull("inherited application scoped bean did not lookup", bean);

    final InheritedApplicationScopedBean beanInst = bean.getInstance();
    assertNotNull("bean instance is null", beanInst);

    final DependentScopedBean bean1 = beanInst.getBean1();
    assertNotNull("bean1 is null", bean1);

    final DependentScopedBeanWithDependencies beanWithDependencies = beanInst.getBeanWithDependencies();
    assertNotNull("beanWithDependencies is null", beanWithDependencies);

    final DependentScopedBean bean2 = beanWithDependencies.getBean();
    assertNotSame("bean1 and bean2 should be different", bean1, bean2);

    final InheritedApplicationScopedBean beanInst2 = bean.getInstance();
    assertSame("bean is not observing application scope", beanInst, beanInst2);
  }

  public void testBeanManagerLookupBeanFromAbstractRootType() {
    final SyncBeanDef<AbstractBean> bean = IOC.getBeanManager().lookupBean(AbstractBean.class);
    assertNotNull("did not find any beans matching", bean);

    final AbstractBean beanInst = bean.getInstance();
    assertNotNull("bean instance is null", beanInst);

    assertTrue("bean is incorrect instance: " + beanInst.getClass(), beanInst instanceof InheritedFromAbstractBean);
  }

  /**
   * This test effectively tests that the IOC container comprehends the full type hierarchy, considering both supertypes
   * and transverse interface types.
   */
  public void testBeanManagerLookupForOuterInterfaceRootType() {
    final SyncBeanDef<OuterBeanInterface> bean = IOC.getBeanManager().lookupBean(OuterBeanInterface.class);
    assertNotNull("did not find any beans matching", bean);

    final OuterBeanInterface beanInst = bean.getInstance();
    assertNotNull("bean instance is null", beanInst);

    assertTrue("bean is incorrect instance: " + beanInst.getClass(), beanInst instanceof InheritedFromAbstractBean);
  }

  public void testBeanManagerLookupForOuterInterfacesOfNonAbstractType() {
    final SyncBeanDef<InterfaceC> beanC = IOC.getBeanManager().lookupBean(InterfaceC.class);
    assertNotNull("did not find any beans matching", beanC);

    final SyncBeanDef<InterfaceD> beanD = IOC.getBeanManager().lookupBean(InterfaceD.class);
    assertNotNull("did not find any beans matching", beanD);
  }

  public void testBeanManagerLookupForExtendedInterfaceType() {
    // This should find ApplicationScopedBeanA, ApplicationScopedBeanB and ApplicationScopedBeanC
    final Collection<SyncBeanDef<InterfaceRoot>> beans = IOC.getBeanManager().lookupBeans(InterfaceRoot.class);
    assertEquals("did not find all managed implementations of " + InterfaceRoot.class.getName(), 3, beans.size());

    // This should find ApplicationScopedBeanA and ApplicationScopedBeanB (InterfaceB extends InterfaceA)
    final Collection<SyncBeanDef<InterfaceA>> beansB = IOC.getBeanManager().lookupBeans(InterfaceA.class);
    assertEquals("did not find both managed implementations of " + InterfaceA.class.getName(), 2, beansB.size());

    // This should find only ApplicationScopedBeanB
    final Collection<SyncBeanDef<InterfaceB>> beansC = IOC.getBeanManager().lookupBeans(InterfaceB.class);
    assertEquals("did not find exactly one managed implementation of " + InterfaceB.class.getName(), 1, beansC.size());
  }

  public void testBeanManagerAPIs() {
    final SyncBeanManager mgr = IOC.getBeanManager();
    final SyncBeanDef<QualAppScopeBeanA> bean = mgr.lookupBean(QualAppScopeBeanA.class, anyAnno);

    final Set<Annotation> a = bean.getQualifiers();
    assertEquals("there should be two qualifiers", 2, a.size());
    assertTrue("wrong qualifiers", annotationSetMatches(a, QualA.class, Any.class));
  }

  public void testQualifiedLookup() {
    final QualA qualA = new QualA() {
      @Override
      public Class<? extends Annotation> annotationType() {
        return QualA.class;
      }
    };

    final QualB qualB = new QualB() {
      @Override
      public Class<? extends Annotation> annotationType() {
        return QualB.class;
      }
    };

    final Collection<SyncBeanDef<CommonInterface>> beans = IOC.getBeanManager().lookupBeans(CommonInterface.class);
    assertEquals("wrong number of beans", 2, beans.size());

    final SyncBeanDef<CommonInterface> beanA = IOC.getBeanManager().lookupBean(CommonInterface.class, qualA);
    assertNotNull("no bean found", beanA);
    assertTrue("wrong bean looked up", beanA.getInstance() instanceof QualAppScopeBeanA);

    final SyncBeanDef<CommonInterface> beanB = IOC.getBeanManager().lookupBean(CommonInterface.class, qualB);
    assertNotNull("no bean found", beanB);
    assertTrue("wrong bean looked up", beanB.getInstance() instanceof QualAppScopeBeanB);
  }

  public void testQualifierLookupWithAnnoAttrib() {
    final QualV qualApples = new QualV() {
      @Override
      public QualEnum value() {
        return QualEnum.APPLES;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return QualV.class;
      }

      @Override
      public int amount() {
        return 5;
      }
    };

    final QualV qualOranges = new QualV() {
      @Override
      public QualEnum value() {
        return QualEnum.ORANGES;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return QualV.class;
      }

      @Override
      public int amount() {
        return 6;
      }
    };

    final Collection<SyncBeanDef<CommonInterfaceB>> beans = IOC.getBeanManager().lookupBeans(CommonInterfaceB.class);
    assertEquals("wrong number of beans", 2, beans.size());

    final SyncBeanDef<CommonInterfaceB> beanA = IOC.getBeanManager().lookupBean(CommonInterfaceB.class, qualApples);
    assertNotNull("no bean found", beanA);
    assertTrue("wrong bean looked up", beanA.getInstance() instanceof QualParmAppScopeBeanApples);

    final SyncBeanDef<CommonInterfaceB> beanB = IOC.getBeanManager().lookupBean(CommonInterfaceB.class, qualOranges);
    assertNotNull("no bean found", beanB);
    assertTrue("wrong bean looked up", beanB.getInstance() instanceof QualParmAppScopeBeanOranges);
  }

  public void testQualifierLookupWithAnnoAttribFailure() {
    final QualV qualOrange = new QualV() {
      @Override
      public QualEnum value() {
        return QualEnum.ORANGES;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return QualV.class;
      }

      @Override
      public int amount() {
        return 5;
      }
    };

    final QualV qualApple = new QualV() {
      @Override
      public QualEnum value() {
        return QualEnum.APPLES;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return QualV.class;
      }

      @Override
      public int amount() {
        return 6;
      }
    };

    final Collection<SyncBeanDef<CommonInterfaceB>> beans = IOC.getBeanManager().lookupBeans(CommonInterfaceB.class);
    assertEquals("wrong number of beans", 2, beans.size());

    final SyncBeanDef<CommonInterfaceB> beanA = IOC.getBeanManager().lookupBean(CommonInterfaceB.class, qualOrange);
    assertNotNull("no bean found", beanA);
    assertFalse("wrong bean looked up", beanA.getInstance() instanceof QualParmAppScopeBeanApples);

    final SyncBeanDef<CommonInterfaceB> beanB = IOC.getBeanManager().lookupBean(CommonInterfaceB.class, qualApple);
    assertNotNull("no bean found", beanB);
    assertFalse("wrong bean looked up", beanB.getInstance() instanceof QualParmAppScopeBeanOranges);
  }

  public void testQualifiedLookupFailure() {
    final LincolnBar wrongAnno = new LincolnBar() {
      @Override
      public Class<? extends Annotation> annotationType() {
        return LincolnBar.class;
      }
    };

    try {
      final SyncBeanDef<CommonInterface> bean = IOC.getBeanManager().lookupBean(CommonInterface.class, anyAnno);
      fail("should have thrown an exception, but got: " + bean);
    }
    catch (final IOCResolutionException e) {
      assertTrue("wrong exception thrown: " + e.getMessage(), e.getMessage().contains("Multiple beans matched"));
    }

    try {
      final SyncBeanDef<CommonInterface> bean = IOC.getBeanManager().lookupBean(CommonInterface.class, wrongAnno);
      fail("should have thrown an exception, but got: " + bean);
    }
    catch (final IOCResolutionException e) {
      assertTrue("wrong exception thrown: " + e.getMessage(), e.getMessage().contains("No beans matched"));
    }
  }

  public void testLookupByName() {
    final Collection<SyncBeanDef> beans = IOC.getBeanManager().lookupBeans("animal");

    assertEquals("wrong number of beans", 2, beans.size());
    assertTrue("should contain a pig", containsInstanceOf(beans, Pig.class));
    assertTrue("should contain a cow", containsInstanceOf(beans, Cow.class));

    for (final SyncBeanDef<?> bean : beans) {
      assertEquals("animal", bean.getName());
    }
  }

  public void testNameAvailableThroughInterfaceLookup() {
    final Collection<SyncBeanDef<CreditCard>> beans = IOC.getBeanManager().lookupBeans(CreditCard.class);
    for (final SyncBeanDef<CreditCard> bean : beans) {
      if (bean.getBeanClass().getName().endsWith("Visa")) {
        assertEquals("visa", bean.getName());
      }
      else if (bean.getBeanClass().getName().endsWith("Amex")) {
        assertEquals("amex", bean.getName());
      }
      else {
        fail("Unexpected bean was returned from lookup: " + bean);
      }
    }
  }

  public void testNameAvailableThroughConcreteTypeLookup() {
    final Collection<SyncBeanDef<Visa>> beans = IOC.getBeanManager().lookupBeans(Visa.class);
    for (final SyncBeanDef<Visa> bean : beans) {
      assertNotNull("Missing name on " + bean, bean.getName());
    }
  }

  public void testLookupAllBeans() {
    final Collection<SyncBeanDef<Object>> beans = IOC.getBeanManager().lookupBeans(Object.class);

    assertTrue(!beans.isEmpty());
  }

  public void testLookupAllBeansQualified() {
    final Collection<SyncBeanDef<Object>> beans = IOC.getBeanManager().lookupBeans(Object.class, QUAL_A);

    assertEquals("Expected one bean but found multiple: " + beans.toString(), 1, beans.size());
    assertEquals(QualAppScopeBeanA.class, beans.iterator().next().getBeanClass());
  }

  public void testReportedScopeCorrect() {
    final SyncBeanDef<ApplicationScopedBean> appScopeBean = IOC.getBeanManager().lookupBean(ApplicationScopedBean.class);
    final SyncBeanDef<DependentScopedBean> dependentIOCBean = IOC.getBeanManager().lookupBean(DependentScopedBean.class);

    assertEquals(ApplicationScoped.class, appScopeBean.getScope());
    assertEquals(Dependent.class, dependentIOCBean.getScope());
  }

  public void testAddingProgrammaticDestructionCallback() {
    final DependentScopedBean dependentScopedBean
        = IOC.getBeanManager().lookupBean(DependentScopedBean.class).newInstance();

    class TestValueHolder {
      boolean destroyed = false;
    }

    final TestValueHolder testValueHolder = new TestValueHolder();

    IOC.getBeanManager().addDestructionCallback(dependentScopedBean, new DestructionCallback<Object>() {
      @Override
      public void destroy(final Object bean) {
        testValueHolder.destroyed = true;
      }
    });

    IOC.getBeanManager().destroyBean(dependentScopedBean);

    assertEquals(true, testValueHolder.destroyed);
  }

  /**
   * Tests that beans marked as Dependent scoped by an IOCExtension can still be forced into a different scope (in this
   * case, ApplicationScoped) when they are annotated as such.
   * <p>
   * Besides this being a good idea on its own, both Errai UI Templates and Errai Navigation rely on this behaviour.
   */
  public void testNormalScopeOverridesDependent() {
    final FoobieScopedBean foobieScopedBean1 = IOC.getBeanManager().lookupBean(FoobieScopedBean.class).getInstance();
    final FoobieScopedBean foobieScopedBean2 = IOC.getBeanManager().lookupBean(FoobieScopedBean.class).getInstance();

    assertNotNull(foobieScopedBean1);
    assertNotSame(foobieScopedBean1, foobieScopedBean2);

    final FoobieScopedOverriddenBean foobieScopedOverriddenBean1
        = IOC.getBeanManager().lookupBean(FoobieScopedOverriddenBean.class).getInstance();

    final FoobieScopedOverriddenBean foobieScopedOverriddenBean2
        = IOC.getBeanManager().lookupBean(FoobieScopedOverriddenBean.class).getInstance();

    assertNotNull(foobieScopedOverriddenBean1);
    assertSame(foobieScopedOverriddenBean1, foobieScopedOverriddenBean2);
  }

  public void testProgrammaticlyRegisteredBeansAreLookedUp() {
    final SyncBeanManager bm = IOC.getBeanManager();
    assertEquals("The disabled alternative must not be in the bean manager before being programmatically added.", 0,
            bm.lookupBeans(DisabledAlternativeBean.class).size());

    bm.registerBean(new SyncBeanDef<DisabledAlternativeBean>() {

      @Override
      public Class<DisabledAlternativeBean> getType() {
        return DisabledAlternativeBean.class;
      }

      @Override
      public Class<?> getBeanClass() {
        return DisabledAlternativeBean.class;
      }

      @Override
      public Class<? extends Annotation> getScope() {
        return Dependent.class;
      }

      @Override
      public DisabledAlternativeBean getInstance() {
        return new DisabledAlternativeBean();
      }

      @Override
      public DisabledAlternativeBean newInstance() {
        return new DisabledAlternativeBean();
      }

      @Override
      public Set<Annotation> getQualifiers() {
        return Collections.emptySet();
      }

      @Override
      public boolean matches(final Set<Annotation> annotations) {
        return true;
      }

      @Override
      public String getName() {
        return "Name of DisabledAlternative";
      }

      @Override
      public boolean isActivated() {
        return true;
      }

      @Override
      public boolean isAssignableTo(final Class<?> type) {
        return Arrays.asList(Object.class, DisabledAlternativeBean.class).contains(type);
      }
    });

    assertEquals("Failed to lookup programmatically added bean by type.", 1, bm.lookupBeans(DisabledAlternativeBean.class).size());
    assertEquals("Failed to lookup programmatically added bean by name.", 1, bm.lookupBeans("Name of DisabledAlternative").size());
  }

  public void testLookupByNameDoesNotFindOtherBeansOfSameType() throws Exception {
    final SyncBeanManager beanManager = IOC.getBeanManager();
    beanManager.registerBean(new SyncBeanDef<InterfaceWithNamedImpls>() {

      @Override
      public Class<InterfaceWithNamedImpls> getType() {
        return InterfaceWithNamedImpls.class;
      }

      @Override
      public Class<?> getBeanClass() {
        return InterfaceWithNamedImpls.class;
      }

      @Override
      public Class<? extends Annotation> getScope() {
        return Dependent.class;
      }

      @Override
      public Set<Annotation> getQualifiers() {
        return Collections.emptySet();
      }

      @Override
      public boolean matches(final Set<Annotation> annotations) {
        return true;
      }

      @Override
      public String getName() {
        return "Programmatic";
      }

      @Override
      public boolean isActivated() {
        return true;
      }

      @Override
      public InterfaceWithNamedImpls getInstance() {
        return null;
      }

      @Override
      public InterfaceWithNamedImpls newInstance() {
        return null;
      }

      @Override
      public boolean isAssignableTo(final Class<?> type) {
        return Arrays.asList(Object.class, InterfaceWithNamedImpls.class).contains(type);
      }
    });

    assertEquals("Found multiple beans looking up @Named bean.", 1, beanManager.lookupBeans("Named").size());
    assertEquals("Found multiple beans looking up programmatically registered bean by name.", 1,
            beanManager.lookupBeans("Programmatic").size());
  }

  private static boolean containsInstanceOf(final Collection<SyncBeanDef> defs, final Class<?> clazz) {
    for (final SyncBeanDef def : defs) {
      if (def.getType().equals(clazz)) return true;
    }
    return false;
  }
}
