/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.cdi.tck.tests.event.fires;

import static org.jboss.cdi.tck.TestGroups.REWRITE;
import static org.jboss.cdi.tck.cdi.Sections.BM_FIRE_EVENT;
import static org.jboss.cdi.tck.cdi.Sections.EVENT;
import static org.jboss.cdi.tck.cdi.Sections.FIRING_EVENTS;
import static org.jboss.cdi.tck.cdi.Sections.FIRING_EVENTS_SYNCHRONOUSLY;
import static org.jboss.cdi.tck.cdi.Sections.OBSERVER_RESOLUTION;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.InjectionPoint;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.cdi.tck.AbstractTest;
import org.jboss.cdi.tck.shrinkwrap.WebArchiveBuilder;
import org.jboss.cdi.tck.tests.extensions.beanManager.broken.event.ContainerLifecycleEvents;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.test.audit.annotations.SpecAssertion;
import org.jboss.test.audit.annotations.SpecAssertions;
import org.jboss.test.audit.annotations.SpecVersion;
import org.testng.annotations.Test;

/**
 * Tests that verify the event firing behavior of the Event interface.
 *
 * @author Dan Allen
 * @author Martin Kouba
 */
@Test
@SpecVersion(spec = "cdi", version = "2.0")
public class FireEventTest extends AbstractTest {

    @Deployment
    public static WebArchive createTestArchive() {
        return new WebArchiveBuilder().withTestClassPackage(FireEventTest.class).withClass(ContainerLifecycleEvents.class).build();
    }

    @SuppressWarnings("serial")
    @Test
    @SpecAssertions({ @SpecAssertion(section = BM_FIRE_EVENT, id = "a"), @SpecAssertion(section = BM_FIRE_EVENT, id = "b") })
    public void testBeanManagerFireEvent() {
        Billing billing = getContextualReference(Billing.class);
        billing.reset();
        MiniBar miniBar = new MiniBar();
        miniBar.stockNoNotify();
        getCurrentManager().fireEvent(miniBar);
        assertTrue(billing.isActive());
        Item chocolate = miniBar.getItemByName("Chocolate");
        getCurrentManager().fireEvent(chocolate, new Lifted.LiftedLiteral() {
        });
        assertEquals(billing.getCharge(), 5);
    }

    @Test(expectedExceptions = { IllegalArgumentException.class })
    @SpecAssertion(section = BM_FIRE_EVENT, id = "c")
    public void testTypeVariableEventTypeFails() throws Exception {
        getContextualReference(Bar.class).<Integer> fireWithTypeVariable();
    }

    @SuppressWarnings("serial")
    @Test(expectedExceptions = { IllegalArgumentException.class })
    @SpecAssertion(section = BM_FIRE_EVENT, id = "d")
    public void testDuplicateBindingsToFireEventFails() throws Exception {
        getCurrentManager().fireEvent(new Object(), new Lifted.LiftedLiteral("a") {
        }, new Lifted.LiftedLiteral("b") {
        });
    }

/**
     * This test verifies that the {@link Event} object capable of firing {@link Item} objects can be injected with the {@link
     * @Any} binding type and that the injected object can be used to fire an event. The functionality is verified by checking
     * that the corresponding observer gets invoked.
     */
    // Simplify assertions
    @Test(groups = REWRITE)
    @SpecAssertion(section = FIRING_EVENTS, id = "a")
    public void testInjectedAnyEventCanFireEvent() {
        Billing billing = getContextualReference(Billing.class);
        billing.reset();
        Bean<MiniBar> miniBarBean = getUniqueBean(MiniBar.class);
        InjectionPoint eventInjection = null;
        for (InjectionPoint candidate : miniBarBean.getInjectionPoints()) {
            if (candidate.getMember().getName().equals("miniBarEvent")) {
                eventInjection = candidate;
                break;
            }
        }

        assertNotNull(eventInjection);
        assertEquals(eventInjection.getQualifiers().size(), 1);
        assertTrue(eventInjection.getQualifiers().contains(Any.Literal.INSTANCE));

        CreationalContext<MiniBar> miniBarCc = getCurrentManager().createCreationalContext(miniBarBean);
        MiniBar miniBar = miniBarBean.create(miniBarCc);
        miniBar.stock();
        assertTrue(billing.isActive());
        assertEquals(billing.getMiniBarValue(), 16);
    }

    /**
     * This test verifies that the fire() method of the injected {@link Event} object accepts an event object and that the event
     * object's type is the same as the the parameterized type on the event field.
     **/
    // Simplify assertions
    @Test(groups = REWRITE)
    @SpecAssertions({ @SpecAssertion(section = FIRING_EVENTS_SYNCHRONOUSLY, id = "b"), @SpecAssertion(section = EVENT, id = "cb") })
    public void testInjectedEventAcceptsEventObject() throws SecurityException, NoSuchFieldException, NoSuchMethodException {
        Billing billing = getContextualReference(Billing.class);
        billing.reset();
        Bean<MiniBar> miniBarBean = getUniqueBean(MiniBar.class);
        CreationalContext<MiniBar> miniBarCc = getCurrentManager().createCreationalContext(miniBarBean);
        MiniBar miniBar = miniBarBean.create(miniBarCc);

        Field eventField = miniBar.getClass().getDeclaredField("miniBarEvent");
        ParameterizedType eventFieldType = (ParameterizedType) eventField.getGenericType();
        assertEquals(eventFieldType.getActualTypeArguments().length, 1);
        assertEquals(MiniBar.class, eventFieldType.getActualTypeArguments()[0]);
        assertEquals(Event.class, eventFieldType.getRawType());
        Method fireMethod = null;
        @SuppressWarnings("unchecked")
        Class<Event<Item>> eventFieldClass = (Class<Event<Item>>) eventFieldType.getRawType();
        for (Method method : eventFieldClass.getMethods()) {
            if (method.getName().equals("fire") && !method.isSynthetic()) {
                if (fireMethod != null) {
                    fail("Expecting exactly one method on Event named 'fire'");
                }
                fireMethod = method;
            }
        }

        if (fireMethod == null) {
            fail("Expecting exactly one method on Event named 'fire'");
        }

        assertEquals(fireMethod.getParameterTypes().length, 1);
        assertEquals(fireMethod.getGenericParameterTypes().length, 1);
        // make sure the same type used to parameterize the Event class is referenced in the fire() method
        Type fireMethodArgumentType = fireMethod.getGenericParameterTypes()[0];
        @SuppressWarnings("rawtypes")
        Type eventClassParameterizedType = ((TypeVariable) fireMethod.getGenericParameterTypes()[0]).getGenericDeclaration()
                .getTypeParameters()[0];
        assertEquals(fireMethodArgumentType, eventClassParameterizedType);

        miniBar.stock();
        assertTrue(billing.isActive());
        assertEquals(billing.getMiniBarValue(), 16);
    }

    /**
     * This test verifies that the {@link Event} object representing an {@link Item} with the {@link @Lifted} binding type is
     * properly injected and that this object can be used to fire an event. The functionality is verified by checking that the
     * cooresponding observer gets invoked.
     */
    @SuppressWarnings("serial")
    // Simplify assertions
    @Test(groups = REWRITE)
    @SpecAssertions({ @SpecAssertion(section = FIRING_EVENTS, id = "c"), @SpecAssertion(section = EVENT, id = "cb") })
    public void testInjectedEventCanHaveBindings() {
        Billing billing = getContextualReference(Billing.class);
        billing.reset();
        Bean<MiniBar> miniBarBean = getUniqueBean(MiniBar.class);

        InjectionPoint eventInjection = null;
        for (InjectionPoint candidate : miniBarBean.getInjectionPoints()) {
            if (candidate.getMember().getName().equals("itemLiftedEvent")) {
                eventInjection = candidate;
                break;
            }
        }

        assertNotNull(eventInjection);
        assertEquals(eventInjection.getQualifiers().size(), 1);
        assertTrue(eventInjection.getQualifiers().contains(new Lifted.LiftedLiteral() {
        }));

        CreationalContext<MiniBar> miniBarCc = getCurrentManager().createCreationalContext(miniBarBean);
        MiniBar miniBar = miniBarBean.create(miniBarCc);
        miniBar.stock();
        Item chocolate = miniBar.getItemByName("Chocolate");
        assertNotNull(chocolate);
        miniBar.liftItem(chocolate);
        assertEquals(billing.getCharge(), chocolate.getPrice());
    }

    /**
     * This test verifies that binding types can be specified dynamically when firing an event using {@link Event#fire()} by
     * first using the {@link Event#select()} method to retrieve an Event object with associated binding types.
     */
    // Simplify assertions
    @Test(groups = REWRITE)
    @SpecAssertion(section = FIRING_EVENTS, id = "d")
    public void testInjectedEventCanSpecifyBindingsDynamically() {
        Billing billing = getContextualReference(Billing.class);
        billing.reset();
        Housekeeping housekeeping = getContextualReference(Housekeeping.class);
        Bean<MiniBar> miniBarBean = getUniqueBean(MiniBar.class);

        InjectionPoint eventInjection = null;
        for (InjectionPoint candidate : miniBarBean.getInjectionPoints()) {
            if (candidate.getMember().getName().equals("itemEvent")) {
                eventInjection = candidate;
                break;
            }
        }

        assertNotNull(eventInjection);
        assertEquals(eventInjection.getQualifiers().size(), 1);
        assertTrue(eventInjection.getQualifiers().contains(Any.Literal.INSTANCE));
        CreationalContext<MiniBar> miniBarCc = getCurrentManager().createCreationalContext(miniBarBean);
        MiniBar miniBar = miniBarBean.create(miniBarCc);
        miniBar.stock();
        Item water = miniBar.liftItemByName("16 oz Water");
        miniBar.restoreItem(water);
        assertEquals(billing.getCharge(), 1);
        assertEquals(housekeeping.getItemsTainted().size(), 1);
        assertTrue(housekeeping.getItemsTainted().contains(water));
    }

    @Test
    @SpecAssertion(section = EVENT, id = "ca")
    public void testEventProvidesMethodForFiringEventsWithCombinationOfTypeAndBindings() {
        DoggiePoints points = getContextualReference(DoggiePoints.class);
        points.reset();
        DogWhisperer master = getContextualReference(DogWhisperer.class);
        master.issueTamingCommand();
        assertEquals(points.getNumTamed(), 1);
        assertEquals(points.getNumPraiseReceived(), 0);
        master.givePraise();
        assertEquals(points.getNumTamed(), 1);
        assertEquals(points.getNumPraiseReceived(), 1);
    }

    // Simplify assertions
    @SuppressWarnings("serial")
    @Test(groups = REWRITE)
    @SpecAssertion(section = EVENT, id = "eda")
    public void testEventSelectedFiresAndObserversNotified() {
        Housekeeping houseKeeping = getContextualReference(Housekeeping.class);
        houseKeeping.reset();
        MiniBar miniBar = getContextualReference(MiniBar.class);
        Item chocolate = new Item("Chocolate", 5);
        Item crackers = new Item("Crackers", 2);

        miniBar.getItemEvent().fire(chocolate);
        assertEquals(houseKeeping.getItemActivity().size(), 1);
        assertEquals(houseKeeping.getItemActivity().get(0), chocolate);

        miniBar.getItemEvent().select(new Lifted.LiftedLiteral() {
        }).fire(crackers);
        assertEquals(houseKeeping.getItemActivity().size(), 2);
        assertEquals(houseKeeping.getItemActivity().get(1), crackers);
        assertEquals(houseKeeping.getItemsMissing().size(), 1);
        assertEquals(houseKeeping.getItemsMissing().iterator().next(), crackers);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    @SpecAssertions({ @SpecAssertion(section = EVENT, id = "f"), @SpecAssertion(section = OBSERVER_RESOLUTION, id = "j") })
    public <T> void testEventFireThrowsExceptionIfEventObjectTypeContainsUnresovableTypeVariable() {
        MiniBar miniBar = getContextualReference(MiniBar.class);
        miniBar.itemEvent.fire(new Item_Illegal<T>("12 oz Beer", 6));
    }

    @Test(dataProvider = ARQUILLIAN_DATA_PROVIDER)
    @SpecAssertion(section = EVENT, id = "g")
    public void testFireContainerLifecycleEvent(ContainerLifecycleEventDispatcher containerLifecycleEvents) {
        containerLifecycleEvents.fireContainerLifecycleEvents();
    }

}
