/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.commons.lang3.event;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.VetoableChangeListener;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;

import javax.naming.event.ObjectChangeListener;

import org.junit.Test;

/**
 * @since 3.0
 */
public class EventUtilsTest 
{

    @Test
    public void testConstructor() {
        assertNotNull(new EventUtils());
        final Constructor<?>[] cons = EventUtils.class.getDeclaredConstructors();
        assertEquals(1, cons.length);
        assertTrue(Modifier.isPublic(cons[0].getModifiers()));
        assertTrue(Modifier.isPublic(EventUtils.class.getModifiers()));
        assertFalse(Modifier.isFinal(EventUtils.class.getModifiers()));
    }
    
    @Test
    public void testAddEventListener()
    {
        final PropertyChangeSource src = new PropertyChangeSource();
        final EventCountingInvociationHandler handler = new EventCountingInvociationHandler();
        final PropertyChangeListener listener = handler.createListener(PropertyChangeListener.class);
        assertEquals(0, handler.getEventCount("propertyChange"));
        EventUtils.addEventListener(src, PropertyChangeListener.class, listener);
        assertEquals(0, handler.getEventCount("propertyChange"));
        src.setProperty("newValue");
        assertEquals(1, handler.getEventCount("propertyChange"));
    }

    @Test
    public void testAddEventListenerWithNoAddMethod()
    {
        final PropertyChangeSource src = new PropertyChangeSource();
        final EventCountingInvociationHandler handler = new EventCountingInvociationHandler();
        final ObjectChangeListener listener = handler.createListener(ObjectChangeListener.class);
        try
        {
            EventUtils.addEventListener(src, ObjectChangeListener.class, listener);
            fail("Should not be allowed to add a listener to an object that doesn't support it.");
        }
        catch (final IllegalArgumentException e)
        {
            assertEquals("Class " + src.getClass().getName() + " does not have a public add" + ObjectChangeListener.class.getSimpleName() + " method which takes a parameter of type " + ObjectChangeListener.class.getName() + ".", e.getMessage());
        }
    }

    @Test
    public void testAddEventListenerThrowsException()
    {
        final ExceptionEventSource src = new ExceptionEventSource();
        try
        {
            EventUtils.addEventListener(src, PropertyChangeListener.class, new PropertyChangeListener()
            {
                @Override
                public void propertyChange(final PropertyChangeEvent e)
                {
                    // Do nothing!
                }
            });
            fail("Add method should have thrown an exception, so method should fail.");
        }
        catch (final RuntimeException e)
        {

        }
    }

    @Test
    public void testAddEventListenerWithPrivateAddMethod()
    {
        final PropertyChangeSource src = new PropertyChangeSource();
        final EventCountingInvociationHandler handler = new EventCountingInvociationHandler();
        final VetoableChangeListener listener = handler.createListener(VetoableChangeListener.class);
        try
        {
            EventUtils.addEventListener(src, VetoableChangeListener.class, listener);
            fail("Should not be allowed to add a listener to an object that doesn't support it.");
        }
        catch (final IllegalArgumentException e)
        {
            assertEquals("Class " + src.getClass().getName() + " does not have a public add" + VetoableChangeListener.class.getSimpleName() + " method which takes a parameter of type " + VetoableChangeListener.class.getName() + ".", e.getMessage());
        }
    }

    @Test
    public void testBindEventsToMethod()
    {
        final PropertyChangeSource src = new PropertyChangeSource();
        final EventCounter counter = new EventCounter();
        EventUtils.bindEventsToMethod(counter, "eventOccurred", src, PropertyChangeListener.class);
        assertEquals(0, counter.getCount());
        src.setProperty("newValue");
        assertEquals(1, counter.getCount());
    }


    @Test
    public void testBindEventsToMethodWithEvent()
    {
        final PropertyChangeSource src = new PropertyChangeSource();
        final EventCounterWithEvent counter = new EventCounterWithEvent();
        EventUtils.bindEventsToMethod(counter, "eventOccurred", src, PropertyChangeListener.class);
        assertEquals(0, counter.getCount());
        src.setProperty("newValue");
        assertEquals(1, counter.getCount());
    }


    @Test
    public void testBindFilteredEventsToMethod()
    {
        final MultipleEventSource src = new MultipleEventSource();
        final EventCounter counter = new EventCounter();
        EventUtils.bindEventsToMethod(counter, "eventOccurred", src, MultipleEventListener.class, "event1");
        assertEquals(0, counter.getCount());
        src.listeners.fire().event1(new PropertyChangeEvent(new Date(), "Day", Integer.valueOf(0), Integer.valueOf(1)));
        assertEquals(1, counter.getCount());
        src.listeners.fire().event2(new PropertyChangeEvent(new Date(), "Day", Integer.valueOf(1), Integer.valueOf(2)));
        assertEquals(1, counter.getCount());
    }

    public static interface MultipleEventListener
    {
        public void event1(PropertyChangeEvent e);

        public void event2(PropertyChangeEvent e);
    }

    public static class EventCounter
    {
        private int count;

        public void eventOccurred()
        {
            count++;
        }

        public int getCount()
        {
            return count;
        }
    }

    public static class EventCounterWithEvent
    {
        private int count;

        public void eventOccurred(final PropertyChangeEvent e)
        {
            count++;
        }

        public int getCount()
        {
            return count;
        }
    }


    private static class EventCountingInvociationHandler implements InvocationHandler
    {
        private final Map<String, Integer> eventCounts = new TreeMap<String, Integer>();

        public <L> L createListener(final Class<L> listenerType)
        {
            return listenerType.cast(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class[]{listenerType},
                    this));
        }

        public int getEventCount(final String eventName)
        {
            final Integer count = eventCounts.get(eventName);
            return count == null ? 0 : count.intValue();
        }

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable
        {
            final Integer count = eventCounts.get(method.getName());
            if (count == null)
            {
                eventCounts.put(method.getName(), Integer.valueOf(1));
            }
            else
            {
                eventCounts.put(method.getName(), Integer.valueOf(count.intValue() + 1));
            }
            return null;
        }
    }

    public static class MultipleEventSource
    {
        private final EventListenerSupport<MultipleEventListener> listeners = EventListenerSupport.create(MultipleEventListener.class);

        public void addMultipleEventListener(final MultipleEventListener listener)
        {
            listeners.addListener(listener);
        }
    }

    public static class ExceptionEventSource
    {
        public void addPropertyChangeListener(final PropertyChangeListener listener)
        {
            throw new RuntimeException();
        }
    }

    public static class PropertyChangeSource
    {
        private final EventListenerSupport<PropertyChangeListener> listeners = EventListenerSupport.create(PropertyChangeListener.class);

        private String property;

        public void setProperty(final String property)
        {
            final String oldValue = this.property;
            this.property = property;
            listeners.fire().propertyChange(new PropertyChangeEvent(this, "property", oldValue, property));
        }

        protected void addVetoableChangeListener(final VetoableChangeListener listener)
        {
            // Do nothing!
        }

        public void addPropertyChangeListener(final PropertyChangeListener listener)
        {
            listeners.addListener(listener);
        }

        public void removePropertyChangeListener(final PropertyChangeListener listener)
        {
            listeners.removeListener(listener);
        }
    }
}
