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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Test class for {@code EventListenerList.}
 */
public class TestEventListenerList {
    /**
     * Test event class. For testing purposes, a small hierarchy of test event class is created. This way it can be checked
     * whether event types are correctly evaluated and take the event hierarchy into account.
     */
    private static class EventBase extends Event {
        private static final long serialVersionUID = 1L;

        /** An event message for testing pay-load. */
        private final String message;

        public EventBase(final Object source, final EventType<? extends EventBase> type, final String msg) {
            super(source, type);
            message = msg;
        }

        public String getMessage() {
            return message;
        }
    }

    /**
     * A test event class derived from the base test event class.
     */
    private static final class EventSub1 extends EventBase {
        private static final long serialVersionUID = 1L;

        public EventSub1(final Object source, final EventType<? extends EventSub1> type, final String msg) {
            super(source, type, msg);
        }
    }

    /**
     * Another test event class derived from the base class.
     */
    private static final class EventSub2 extends EventBase {
        private static final long serialVersionUID = 1L;

        public EventSub2(final Object source, final EventType<? extends EventSub2> type, final String msg) {
            super(source, type, msg);
        }
    }

    /**
     * A test event listener implementation. This listener class expects that it receives at most a single event. This event
     * is stored for further evaluation.
     */
    private static final class ListenerTestImpl implements EventListener<EventBase> {
        /** The event received by this object. */
        private EventBase receivedEvent;

        /**
         * Checks that this listener has received an event with the expected properties.
         *
         * @param expSource the expected source
         * @param expType the expected type
         * @param expMessage the expected message
         */
        public void assertEvent(final Object expSource, final EventType<?> expType, final String expMessage) {
            assertNotNull(receivedEvent);
            assertEquals(expSource, receivedEvent.getSource());
            assertEquals(expType, receivedEvent.getEventType());
            assertEquals(expMessage, receivedEvent.getMessage());
        }

        /**
         * Checks that this listener has not received any event.
         */
        public void assertNoEvent() {
            assertNull(receivedEvent);
        }

        @Override
        public void onEvent(final EventBase event) {
            assertNull(receivedEvent, "Too many events: " + event);
            receivedEvent = event;
        }
    }

    /** Constant for a test event message. */
    private static final String MESSAGE = "TestEventMessage";

    /** Type for the base event. */
    private static EventType<EventBase> typeBase;

    /** Type for sub event 1. */
    private static EventType<EventSub1> typeSub1;

    /** Type for sub event 2. */
    private static EventType<EventSub2> typeSub2;

    /**
     * Helper method for collecting the elements in the given iterable.
     *
     * @param iterable the iterable
     * @return a list with the content of the iterable
     */
    private static <T> List<T> fetchElements(final Iterable<? extends T> iterable) {
        final List<T> elems = new LinkedList<>();
        for (final T listener : iterable) {
            elems.add(listener);
        }
        return elems;
    }

    @BeforeAll
    public static void setUpBeforeClass() throws Exception {
        typeBase = new EventType<>(Event.ANY, "BASE");
        typeSub1 = new EventType<>(typeBase, "SUB1");
        typeSub2 = new EventType<>(typeBase, "SUB2");
    }

    /** The list to be tested. */
    private EventListenerList list;

    /**
     * Helper method for checking whether a specific set of event listeners is returned by getEventListeners().
     *
     * @param eventType the event type
     * @param expListeners the expected listeners
     */
    private void checkEventListenersForType(final EventType<? extends Event> eventType, final EventListener<?>... expListeners) {
        final List<?> listeners = fetchElements(list.getEventListeners(eventType));
        assertEquals(Arrays.asList(expListeners), listeners);
    }

    @BeforeEach
    public void setUp() throws Exception {
        list = new EventListenerList();
    }

    /**
     * Tests whether the content of another list can be added.
     */
    @Test
    public void testAddAll() {
        final EventListener<EventBase> l1 = new ListenerTestImpl();
        final EventListener<EventBase> l2 = new ListenerTestImpl();
        final EventListener<EventBase> l3 = new ListenerTestImpl();
        list.addEventListener(typeBase, l1);
        final EventListenerList list2 = new EventListenerList();
        list2.addEventListener(typeSub1, l2);
        list2.addEventListener(typeBase, l3);

        list.addAll(list2);
        final Iterator<EventListenerRegistrationData<?>> it = list.getRegistrations().iterator();
        EventListenerRegistrationData<?> reg = it.next();
        assertEquals(typeBase, reg.getEventType());
        assertEquals(l1, reg.getListener());
        reg = it.next();
        assertEquals(typeSub1, reg.getEventType());
        assertEquals(l2, reg.getListener());
        reg = it.next();
        assertEquals(typeBase, reg.getEventType());
        assertEquals(l3, reg.getListener());
    }

    /**
     * Tries to add the content of a null list.
     */
    @Test
    public void testAddAllNull() {
        assertThrows(IllegalArgumentException.class, () -> list.addAll(null));
    }

    /**
     * Tests whether the list can be cleared.
     */
    @Test
    public void testClear() {
        list.addEventListener(typeSub1, new ListenerTestImpl());
        list.addEventListener(typeSub2, new ListenerTestImpl());

        list.clear();
        assertTrue(list.getRegistrations().isEmpty());
    }

    /**
     * Tests that a null event is handled by the iterator.
     */
    @Test
    public void testEventListenerIteratorNullEvent() {
        list.addEventListener(typeBase, new ListenerTestImpl());
        final EventListenerList.EventListenerIterator<EventBase> iterator = list.getEventListenerIterator(typeBase);
        assertThrows(IllegalArgumentException.class, () -> iterator.invokeNext(null));
    }

    /**
     * Tests whether the event listener iterator validates the passed in event object.
     */
    @Test
    public void testEventListenerIteratorWrongEvent() {
        final EventListener<EventSub2> listener = event -> { };
        list.addEventListener(typeSub2, listener);
        final EventListenerList.EventListenerIterator<EventSub2> iterator = list.getEventListenerIterator(typeSub2);
        assertTrue(iterator.hasNext());
        final Event event = new EventBase(this, typeBase, "Test");
        assertThrows(IllegalArgumentException.class, () -> iterator.invokeNext(event));
    }

    /**
     * Tests that a null event is rejected by fire().
     */
    @Test
    public void testFireNullEvent() {
        assertThrows(IllegalArgumentException.class, () -> list.fire(null));
    }

    /**
     * Tests whether event listener registrations derived from a super type can be queried.
     */
    @Test
    public void testGetEventListenerRegistrationsForSuperType() {
        final ListenerTestImpl l1 = new ListenerTestImpl();
        final ListenerTestImpl l2 = new ListenerTestImpl();
        @SuppressWarnings("unchecked")
        final EventListener<Event> l3 = mock(EventListener.class);
        list.addEventListener(typeSub1, l1);
        list.addEventListener(Event.ANY, l3);
        list.addEventListener(typeBase, l2);

        final List<EventListenerRegistrationData<? extends EventBase>> regs = list.getRegistrationsForSuperType(typeBase);
        final Iterator<EventListenerRegistrationData<? extends EventBase>> iterator = regs.iterator();
        assertEquals(l1, iterator.next().getListener());
        assertEquals(l2, iterator.next().getListener());
        assertFalse(iterator.hasNext());
    }

    /**
     * Tests whether the base type is taken into account when querying for event listeners.
     */
    @Test
    public void testGetEventListenersBaseType() {
        final ListenerTestImpl listener1 = new ListenerTestImpl();
        final ListenerTestImpl listener2 = new ListenerTestImpl();
        list.addEventListener(typeBase, listener1);
        list.addEventListener(typeBase, listener2);
        checkEventListenersForType(typeSub1, listener1, listener2);
    }

    /**
     * Tests that the iterator returned by getEventListeners() throws an exception if the iteration goes beyond the last
     * element.
     */
    @Test
    public void testGetEventListenersIteratorNextNoElement() {
        final ListenerTestImpl listener1 = new ListenerTestImpl();
        final ListenerTestImpl listener2 = new ListenerTestImpl();
        list.addEventListener(typeBase, listener1);
        list.addEventListener(typeBase, listener2);
        final Iterator<EventListener<? super EventBase>> iterator = list.getEventListeners(typeBase).iterator();
        for (int i = 0; i < 2; i++) {
            iterator.next();
        }
        assertThrows(NoSuchElementException.class, iterator::next);
    }

    /**
     * Tests that the iterator returned by getEventListeners() does not support remove() operations.
     */
    @Test
    public void testGetEventListenersIteratorRemove() {
        list.addEventListener(typeBase, new ListenerTestImpl());
        final Iterator<EventListener<? super EventBase>> iterator = list.getEventListeners(typeBase).iterator();
        assertTrue(iterator.hasNext());
        assertThrows(UnsupportedOperationException.class, iterator::remove);
    }

    /**
     * Tests whether only matching event listeners are returned by getEventListeners().
     */
    @Test
    public void testGetEventListenersMatchingType() {
        final ListenerTestImpl listener1 = new ListenerTestImpl();
        final ListenerTestImpl listener2 = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener1);
        list.addEventListener(typeSub2, listener2);
        checkEventListenersForType(typeSub1, listener1);
    }

    /**
     * Tests that an empty result is correctly handled by getEventListeners().
     */
    @Test
    public void testGetEventListenersNoMatch() {
        list.addEventListener(typeSub1, new ListenerTestImpl());
        checkEventListenersForType(typeSub2);
    }

    /**
     * Tests whether event listeners for a null type can be queried.
     */
    @Test
    public void testGetEventListenersNull() {
        assertTrue(fetchElements(list.getEventListeners(null)).isEmpty());
    }

    /**
     * Tests whether all event listener registrations can be queried.
     */
    @Test
    public void testGetRegistrations() {
        final EventListenerRegistrationData<EventSub1> reg1 = new EventListenerRegistrationData<>(typeSub1, new ListenerTestImpl());
        final EventListenerRegistrationData<EventSub2> reg2 = new EventListenerRegistrationData<>(typeSub2, new ListenerTestImpl());
        list.addEventListener(reg1);
        list.addEventListener(reg2);

        final List<EventListenerRegistrationData<?>> registrations = list.getRegistrations();
        assertEquals(Arrays.asList(reg1, reg2), registrations);
    }

    /**
     * Tests that the list with registration information cannot be modified.
     */
    @Test
    public void testGetRegistrationsModify() {
        final EventListenerRegistrationData<EventBase> registrationData = new EventListenerRegistrationData<>(typeBase, new ListenerTestImpl());
        final List<EventListenerRegistrationData<?>> registrations = list.getRegistrations();
        assertThrows(UnsupportedOperationException.class, () -> registrations.add(registrationData));
    }

    /**
     * Tests whether an event listener can be registered via a registration data object.
     */
    @Test
    public void testListenerRegistrationWithListenerData() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        final EventListenerRegistrationData<EventSub1> regData = new EventListenerRegistrationData<>(typeSub1, listener);
        list.addEventListener(regData);

        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener.assertEvent(this, typeSub1, MESSAGE);
    }

    /**
     * Tries to register a listener with a null registration data object.
     */
    @Test
    public void testListenerRegistrationWithNullListenerData() {
        assertThrows(IllegalArgumentException.class, () -> list.addEventListener(null));
    }

    /**
     * Tests that a listener can be registered multiple times for different event types.
     */
    @Test
    public void testMultipleListenerRegistration() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener);
        list.addEventListener(typeSub2, listener);

        list.fire(new EventSub2(this, typeSub2, MESSAGE));
        list.removeEventListener(typeSub1, listener);
        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener.assertEvent(this, typeSub2, MESSAGE);
    }

    /**
     * Tests whether the event type is taken into account when calling listeners.
     */
    @Test
    public void testReceiveEventDifferentType() {
        final ListenerTestImpl listener1 = new ListenerTestImpl();
        final ListenerTestImpl listener2 = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener1);
        list.addEventListener(typeSub2, listener2);

        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener1.assertEvent(this, typeSub1, MESSAGE);
        listener2.assertNoEvent();
    }

    /**
     * Tests whether multiple event listeners can be registered.
     */
    @Test
    public void testReceiveEventMultipleListeners() {
        final ListenerTestImpl listener1 = new ListenerTestImpl();
        final ListenerTestImpl listener2 = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener1);
        list.addEventListener(typeSub1, listener2);

        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener1.assertEvent(this, typeSub1, MESSAGE);
        listener2.assertEvent(this, typeSub1, MESSAGE);
    }

    /**
     * Tests whether events matching the registration type are delivered.
     */
    @Test
    public void testReceiveEventOfExactType() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener);

        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener.assertEvent(this, typeSub1, MESSAGE);
    }

    /**
     * Tests that events of a derived type are delivered to listeners registered for a base type.
     */
    @Test
    public void testReceiveEventSubType() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        list.addEventListener(typeBase, listener);

        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener.assertEvent(this, typeSub1, MESSAGE);
    }

    /**
     * Tries to register a listener for a null event type.
     */
    @Test
    public void testRegisterEventTypeNull() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        assertThrows(IllegalArgumentException.class, () -> list.addEventListener(null, listener));
    }

    /**
     * Tests that null event listeners cannot be registered.
     */
    @Test
    public void testRegisterListenerNull() {
        assertThrows(IllegalArgumentException.class, () -> list.addEventListener(typeBase, null));
    }

    /**
     * Tests whether an event listener can be removed.
     */
    @Test
    public void testRemoveEventListenerExisting() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener);

        assertTrue(list.removeEventListener(typeSub1, listener));
        list.fire(new EventSub1(this, typeSub1, MESSAGE));
        listener.assertNoEvent();
    }

    /**
     * Tests removeEventListener() if another event type is specified for an existing listener.
     */
    @Test
    public void testRemoveEventListenerNonExistingEventType() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener);

        assertFalse(list.removeEventListener(typeBase, listener));
    }

    /**
     * Tests removeEventListener() for a non-existing event listener.
     */
    @Test
    public void testRemoveEventListenerNonExistingListener() {
        list.addEventListener(typeBase, new ListenerTestImpl());
        assertFalse(list.removeEventListener(typeBase, new ListenerTestImpl()));
    }

    /**
     * Tests that removeEventListener() can handle a null listener.
     */
    @Test
    public void testRemoveEventListenerNullListener() {
        assertFalse(list.removeEventListener(typeBase, null));
    }

    /**
     * Tests that removeEventListener() can handle a null registration object.
     */
    @Test
    public void testRemoveEventListenerNullRegistration() {
        assertFalse(list.removeEventListener(null));
    }

    /**
     * Tests that removeEventListener() can handle a null event type.
     */
    @Test
    public void testRemoveEventListenerNullType() {
        assertFalse(list.removeEventListener(null, new ListenerTestImpl()));
    }

    /**
     * Tests that events of a base type do not cause a listener to be invoked.
     */
    @Test
    public void testSuppressEventOfSuperType() {
        final ListenerTestImpl listener = new ListenerTestImpl();
        list.addEventListener(typeSub1, listener);

        list.fire(new EventBase(this, typeBase, MESSAGE));
        listener.assertNoEvent();
    }
}
