package de.accso.flexinale.common.api.eventbus;

import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.*;
import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent;
import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent;
import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent;
import de.accso.flexinale.common.infrastructure.eventbus.*;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.*;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

class InMemorySyncEventBusAndSpyTest {

    @Test
    void testCreateAndGetEventBus() {
        // arrange
        InMemorySyncEventBusFactory sut = new InMemorySyncEventBusFactory();

        // act
        InMemorySyncEventBus<MyNumberEvent> eventBus =
                (InMemorySyncEventBus<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);
        InMemorySyncEventBus<MyNumberEvent> retrievedEventBus =
                (InMemorySyncEventBus<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);

        // assert
        assertThat(eventBus).isNotNull();
        assertThat(retrievedEventBus).isEqualTo(eventBus);
    }

    @Test
    void testCreateAndGetEventBusSpy() {
        // arrange
        InMemorySyncEventBusSpyFactory sut = new InMemorySyncEventBusSpyFactory();

        // act
        InMemorySyncEventBusSpy<MyNumberEvent> eventBus =
                (InMemorySyncEventBusSpy<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);
        InMemorySyncEventBusSpy<MyNumberEvent> retrievedEventBus =
                (InMemorySyncEventBusSpy<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);

        // assert
        assertThat(eventBus).isNotNull();
        assertThat(retrievedEventBus).isEqualTo(eventBus);
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testSubscriberReceivesEventOfCorrectType() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        internal_testSubscriberReceivesEventOfCorrectType(sut);
    }

    @Test
    void testSubscriberReceivesEventOfCorrectTypeSpy() {
        // arrange
        InMemorySyncEventBusSpyFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEvent myEvent = internal_testSubscriberReceivesEventOfCorrectType(sut);

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
        assertThat(spy.allEvents()).isEqualTo(List.of(myEvent));
        assertThat(spy.size()).isEqualTo(1);
    }

    private MyNumberEvent internal_testSubscriberReceivesEventOfCorrectType(final EventBus<MyNumberEvent> sut) {
        MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
        MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);

        // act
        sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
        MyNumberEvent myEvent = new MyNumberEvent(11, 22);
        myPublisher.post(MyNumberEvent.class, myEvent);

        // assert
        assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
        assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1);
        return myEvent;
    }

    @SuppressWarnings("unchecked")
    @Test
    void testEventSubscriberProducesRuntimeException() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEventPublisher myNumberPublisher = new MyNumberEventPublisher(numberBus);

        MyNumberEventThrowingRuntimeExceptionSubscriber erroneousSubscriber = new MyNumberEventThrowingRuntimeExceptionSubscriber();
        numberBus.subscribe(MyNumberEvent.class, (EventSubscriber) erroneousSubscriber, START_READING_FROM_NOW);

        MyNumberEvent myNumberEvent = new MyNumberEvent(11, 22);
        Identifiable.Id id = myNumberEvent.id();

        // act and assert that receive method throws RuntimeException (wrapped in a RuntimeException by the dispatcher)
        Exception caughtException = null;

        try {
            myNumberPublisher.post(MyNumberEvent.class, myNumberEvent);
        } catch (Exception ex) {
            caughtException = ex;
        }
        assertThat(caughtException).isNotNull(); // assertThatThrownBy would be nicer, but we need to the check the exception's contents

        Throwable cause = caughtException.getCause();
        // ... and wrapped again in a InvocationTargetException
        assertThat(cause).isInstanceOf(java.lang.reflect.InvocationTargetException.class);
        // so here we go with the root cause:
        assertThat(cause.getCause()).isInstanceOf(MyNumberEventRuntimeException.class);
        assertThat(cause.getCause()).hasMessageContaining("RuntimeException for event with Id=" + id);
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testGenericSubscriberReceivesAllEventsIndependentOfItsType() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
        EventBus<MyTextEvent> textBus = factory.createOrGetEventBusFor(MyTextEvent.class);

        MyNumberEventSubscriber myNumberSubscriber = new MyNumberEventSubscriber();
        MyNumberEventPublisher myNumberPublisher = new MyNumberEventPublisher(numberBus);
        MyTextEventSubscriber myTextSubscriber = new MyTextEventSubscriber();
        MyTextEventPublisher myTextPublisher = new MyTextEventPublisher(textBus);

        numberBus.subscribe(MyNumberEvent.class, myNumberSubscriber, START_READING_FROM_NOW);
        textBus.subscribe(MyTextEvent.class, myTextSubscriber, START_READING_FROM_NOW);

        GenericEventSubscriber myGenericSubscriber = new GenericEventSubscriber();

        numberBus.subscribe(myGenericSubscriber, START_READING_FROM_NOW);
        textBus.subscribe(myGenericSubscriber, START_READING_FROM_NOW);

        // act
        MyNumberEvent myNumberEvent = new MyNumberEvent(11, 22);
        myNumberPublisher.post(MyNumberEvent.class, myNumberEvent);

        // assert
        assertThat(myNumberSubscriber.lastReceivedEvent).isEqualTo(myNumberEvent);
        assertThat(myNumberSubscriber.counterOfRetrievedEvents).isEqualTo(1);

        assertThat(myGenericSubscriber.lastReceivedEvent.getClass()).isEqualTo(MyNumberEvent.class);
        assertThat(myGenericSubscriber.lastReceivedEvent).isEqualTo(myNumberEvent);
        assertThat(myGenericSubscriber.counterOfRetrievedEvents).isEqualTo(1);

        // act
        MyTextEvent myTextEvent = new MyTextEvent("11", "22");
        myTextPublisher.post(MyTextEvent.class, myTextEvent);

        // assert
        assertThat(myTextSubscriber.lastReceivedEvent).isEqualTo(myTextEvent);
        assertThat(myTextSubscriber.counterOfRetrievedEvents).isEqualTo(1);

        assertThat(myGenericSubscriber.lastReceivedEvent.getClass()).isEqualTo(MyTextEvent.class);
        assertThat(myGenericSubscriber.lastReceivedEvent).isEqualTo(myTextEvent);
        assertThat(myGenericSubscriber.counterOfRetrievedEvents).isEqualTo(2);
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testEventChainKeepsCorrelationIdAndEventHistory() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();

        EventBus<MyTextEvent> textBus = factory.createOrGetEventBusFor(MyTextEvent.class);
        EventBus<MyNumberEvent> numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEventSubscriberAndMyTextEventPublisher numberSubscriberAndTextPublisher =
                new MyNumberEventSubscriberAndMyTextEventPublisher(textBus);
        MyTextEventSubscriber myTextEventSubscriber = new MyTextEventSubscriber();

        numberBus.subscribe(MyNumberEvent.class, numberSubscriberAndTextPublisher, START_READING_FROM_NOW);
        textBus.subscribe(MyTextEvent.class, myTextEventSubscriber, START_READING_FROM_NOW);

        // act - send MyNumberEvent so that internally a MyTextEvent is sent
        MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
        new MyNumberEventPublisher(numberBus).post(MyNumberEvent.class, firstEvent);
        assertThat(numberSubscriberAndTextPublisher.lastReceivedEvent).isEqualTo(firstEvent);

        // assert that the event chain keeps the correlation Id and the event history
        assertThat(numberSubscriberAndTextPublisher.lastSentEvent).isEqualTo(myTextEventSubscriber.lastReceivedEvent);
        MyTextEvent secondEvent = numberSubscriberAndTextPublisher.lastSentEvent;

        assertThat(secondEvent.correlationId()).isEqualTo(firstEvent.correlationId());
        assertThat(secondEvent.eventContext().eventHistory.getFirst().id()).isEqualTo(firstEvent.id());
        assertThat(secondEvent.eventContext().eventHistory.getFirst().eventType()).isEqualTo(MyNumberEvent.class);
        assertThat(secondEvent.text1).isEqualTo("11");
        assertThat(secondEvent.text2).isEqualTo("22");

        // assert that the context of the bus is cleared
        assertThat(textBus.getEventContextHolder().isEmpty()).isTrue();
        assertThat(numberBus.getEventContextHolder().isEmpty()).isTrue();
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testSubscriberReceivesMultipleEvents() {
        // arrange
        int numEventsToPublish = 100;

        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        internal_testSubscriberReceivesMultipleEvents(numEventsToPublish, sut);
    }

    @Test
    void testSubscriberReceivesMultipleEventsSpy() {
        // arrange
        int numEventsToPublish = 100;

        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEvent myEvent = internal_testSubscriberReceivesMultipleEvents(numEventsToPublish, sut);

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
        List<MyNumberEvent> allEvents = spy.allEvents();
        assertThat(allEvents.getLast()).isEqualTo(myEvent);
        assertThat(spy.size()).isEqualTo(numEventsToPublish);
    }

    private static MyNumberEvent internal_testSubscriberReceivesMultipleEvents(
            final int numEventsToPublish, final EventBus<MyNumberEvent> sut) {

        MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true);
        MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);

        // act
        sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
        MyNumberEvent myEvent = null;
        for (int counter = 0; counter < numEventsToPublish; counter++) {
            myEvent = new MyNumberEvent(counter, counter);
            myPublisher.post(MyNumberEvent.class, myEvent);
        }

        // assert
        assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
        assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
        return myEvent;
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testSubscriberDoesNotReceiveOtherEventTypes() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> bus1 = factory.createOrGetEventBusFor(MyNumberEvent.class);
        EventBus<MyMixedEvent> bus2 = factory.createOrGetEventBusFor(MyMixedEvent.class);

        internal_testSubscriberDoesNotReceiveOtherEventTypes(bus1, bus2);
    }

    @Test
    void testSubscriberDoesNotReceiveOtherEventTypesSpy() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> bus1 = factory.createOrGetEventBusFor(MyNumberEvent.class);
        EventBus<MyMixedEvent> bus2 = factory.createOrGetEventBusFor(MyMixedEvent.class);

        MyMixedEvent myEvent = internal_testSubscriberDoesNotReceiveOtherEventTypes(bus1, bus2);

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> spy1 = (InMemorySyncEventBusSpy<MyNumberEvent>) bus1;
        InMemorySyncEventBusSpy<MyMixedEvent> spy2 = (InMemorySyncEventBusSpy<MyMixedEvent>) bus2;
        assertThat(spy1.allEvents().isEmpty()).isTrue();
        assertThat(spy1.size()).isEqualTo(0);
        assertThat(spy2.allEvents()).isEqualTo(List.of(myEvent));
        assertThat(spy2.size()).isEqualTo(1);
    }

    private static MyMixedEvent internal_testSubscriberDoesNotReceiveOtherEventTypes(
            final EventBus<MyNumberEvent> bus1, final EventBus<MyMixedEvent> bus2) {

        MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
        MyMixedEventPublisher myPublisher = new MyMixedEventPublisher(bus2);

        // act
        bus1.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
        MyMixedEvent myEvent = new MyMixedEvent("abc", 22);
        myPublisher.post(MyMixedEvent.class, myEvent);

        // assert
        assertThat(mySubscriber.lastReceivedEvent).isNull();
        assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(0);
        return myEvent;
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testSubscriberReceivedEventsOnlyAfterSubscribing() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
        MyNumberEvent secondEvent = new MyNumberEvent(12, 23);

        internal_testSubscriberReceivedEventsOnlyAfterSubscribing(sut, firstEvent, secondEvent);
    }

    @Test
    void testSubscriberReceivedEventsOnlyAfterSubscribingSpy() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
        MyNumberEvent secondEvent = new MyNumberEvent(12, 23);

        internal_testSubscriberReceivedEventsOnlyAfterSubscribing(sut, firstEvent, secondEvent);

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
        assertThat(spy.allEvents()).isEqualTo(List.of(firstEvent, secondEvent));
    }

    private static void internal_testSubscriberReceivedEventsOnlyAfterSubscribing(
            final EventBus<MyNumberEvent> sut,
            final MyNumberEvent firstEvent, final MyNumberEvent secondEvent) {
        MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
        MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);

        // act
        myPublisher.post(MyNumberEvent.class, firstEvent); // published but not received

        sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);

        myPublisher.post(MyNumberEvent.class, secondEvent); // published and received

        // assert
        assertThat(mySubscriber.lastReceivedEvent).isEqualTo(secondEvent);
        assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1);
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testSubscriberReceivesNoMoreEventsAfterUnsubscribing() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
        MyNumberEvent secondEvent = new MyNumberEvent(12, 23);

        internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(sut, firstEvent, secondEvent);
    }

    @Test
    void testSubscriberReceivesNoMoreEventsAfterUnsubscribingSpy() {
        // arrange
        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
        MyNumberEvent secondEvent = new MyNumberEvent(12, 23);

        internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(sut, firstEvent, secondEvent);

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
        assertThat(spy.allEvents()).isEqualTo(List.of(firstEvent, secondEvent));
    }

    private static void internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(
            final EventBus<MyNumberEvent> sut,
            final MyNumberEvent firstEvent, final MyNumberEvent secondEvent) {
        MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
        MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);

        // act
        sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
        myPublisher.post(MyNumberEvent.class, firstEvent); // published but not received
        assertThat(mySubscriber.lastReceivedEvent).isEqualTo(firstEvent);
        sut.unsubscribe(MyNumberEvent.class, mySubscriber);
        myPublisher.post(MyNumberEvent.class, secondEvent); // published and received

        // assert
        assertThat(mySubscriber.lastReceivedEvent).isEqualTo(firstEvent);
        assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1);
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testMultipleSubscribersInSeparateGroupsAllReceiveTheSameEvents() {
        // arrange
        int numSubscribers = 5;

        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        List<MyNumberEvent> events = List.of(new MyNumberEvent(11, 22), new MyNumberEvent(12, 23),
                new MyNumberEvent(13, 24), new MyNumberEvent(14, 25));

        internal_testListOfSubscribersAllReceiveTheSameEvents(numSubscribers, sut, events.toArray(new MyNumberEvent[0]));
    }

    @Test
    void testMultipleSubscribersInSeparateGroupsAllReceiveTheSameEventsSpy() {
        // arrange
        int numSubscribers = 5;

        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);

        List<MyNumberEvent> events = List.of(new MyNumberEvent(11, 22), new MyNumberEvent(12, 23),
                new MyNumberEvent(13, 24), new MyNumberEvent(14, 25));

        internal_testListOfSubscribersAllReceiveTheSameEvents(numSubscribers, sut, events.toArray(new MyNumberEvent[0]));

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
        assertThat(spy.allEvents()).isEqualTo(events);
    }

    private static void internal_testListOfSubscribersAllReceiveTheSameEvents(
            final int numSubscribers, final EventBus<MyNumberEvent> sut,
            final MyNumberEvent... events) {

        List<MyNumberEventSubscriber> mySubscribers = new ArrayList<>();
        for (int counter = 0; counter < numSubscribers; counter++) {
            MyEventSubscriberEachInstanceInItsOwnGroup mySubscriberEachInItsOwnGroup = new MyEventSubscriberEachInstanceInItsOwnGroup(true);
            mySubscribers.add(mySubscriberEachInItsOwnGroup);
            sut.subscribe(MyNumberEvent.class, mySubscriberEachInItsOwnGroup, START_READING_FROM_NOW);
        }

        MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);

        for (MyNumberEvent event: events) {
            // act - first event
            myPublisher.post(MyNumberEvent.class, event);

            // assert
            for (MyNumberEventSubscriber mySubscriber : mySubscribers) {
                assertThat(mySubscriber.lastReceivedEvent).isEqualTo(event);
            }
        }
    }

    // -----------------------------------------------------------------------------------------------------------

    @Test
    void testMultipleSubscribersReceiveMultipleEventsForMultipleTypes() {
        // arrange
        int numEventsToPublish = 9_999;
        int numSubscribersPerType = 1_999;

        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
        EventBus<MyNumberEvent> numberEventBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
        EventBus<MyMixedEvent> mixedEventBus = factory.createOrGetEventBusFor(MyMixedEvent.class);

        // create subscribers
        internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(numEventsToPublish, numSubscribersPerType,
                numberEventBus, mixedEventBus);
    }

    @Test
    void testMultipleSubscribersReceiveMultipleEventsForMultipleTypesSpy() {
        // arrange
        int numEventsToPublish = 9_999;
        int numSubscribersPerType = 1_999;

        InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
        EventBus<MyNumberEvent> numberEventBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
        EventBus<MyMixedEvent> mixedEventBus = factory.createOrGetEventBusFor(MyMixedEvent.class);

        internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(numEventsToPublish, numSubscribersPerType,
                numberEventBus, mixedEventBus);

        // ... and assert spy functionality
        InMemorySyncEventBusSpy<MyNumberEvent> numberEventBusSpy = (InMemorySyncEventBusSpy<MyNumberEvent>) numberEventBus;
        InMemorySyncEventBusSpy<MyMixedEvent> mixedEventBusSpy = (InMemorySyncEventBusSpy<MyMixedEvent>) mixedEventBus;
        assertThat(numberEventBusSpy.size()).isEqualTo(numEventsToPublish);
        assertThat(mixedEventBusSpy.size()).isEqualTo(numEventsToPublish);
    }

    private static void internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(
            final int numEventsToPublish, final int numSubscribersPerType,
            final EventBus<MyNumberEvent> numberEventBus, final EventBus<MyMixedEvent> mixedEventBus) {

        // LocalDateTime start = LocalDateTime.now();

        Random random = new Random();

        // create subscribers, all instances in the same group
        List<MyNumberEventSubscriber> myNumberEventSubscribers = new ArrayList<>();
        List<MyMixedEventSubscriber> myMixedEventSubscribers = new ArrayList<>();

        // create subscribers, all instances in the separate groups (each in one group)
        List<MyEventSubscriberEachInstanceInItsOwnGroup> mySubscribersEachInItsOwnGroup = new ArrayList<>();

        for (int counter = 0; counter < numSubscribersPerType; counter++) {
            MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
            myNumberEventSubscribers.add(mySubscriber);
            numberEventBus.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
        }
        for (int counter = 0; counter < numSubscribersPerType; counter++) {
            MyMixedEventSubscriber mySubscriber = new MyMixedEventSubscriber();
            myMixedEventSubscribers.add(mySubscriber);
            mixedEventBus.subscribe(MyMixedEvent.class, mySubscriber, START_READING_FROM_NOW);
        }
        for (int counter = 0; counter < numSubscribersPerType; counter++) {
            MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber = new MyEventSubscriberEachInstanceInItsOwnGroup();
            mySubscribersEachInItsOwnGroup.add(mySubscriber);
            numberEventBus.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
        }

        // create publisher and post events
        MyNumberEventPublisher myNumberEventPublisher = new MyNumberEventPublisher(numberEventBus);
        MyMixedEventPublisher  myMixedEventPublisher  = new MyMixedEventPublisher(mixedEventBus);

        MyNumberEvent lastNumberEvent = null;
        for (int eventCounter = 0; eventCounter < numEventsToPublish; eventCounter++) {
            lastNumberEvent = new MyNumberEvent(random.nextInt(), random.nextInt());
            myNumberEventPublisher.post(MyNumberEvent.class, lastNumberEvent);
        }
        MyMixedEvent lastMixedEvent = null;
        for (int eventCounter = 0; eventCounter < numEventsToPublish; eventCounter++) {
            lastMixedEvent = new MyMixedEvent("" + random.nextInt(), random.nextInt());
            myMixedEventPublisher.post(MyMixedEvent.class, lastMixedEvent);
        }

        // assert
        List<MyNumberEvent> lastNumberReceivedEvents = new ArrayList<>();
        int counterOfRetrievedNumberEvents = 0;
        for (MyNumberEventSubscriber mySubscriber : myNumberEventSubscribers) {
            lastNumberReceivedEvents.add(mySubscriber.lastReceivedEvent);
            counterOfRetrievedNumberEvents += mySubscriber.counterOfRetrievedEvents;
        }
        assertThat(lastNumberReceivedEvents.contains(lastNumberEvent)).isTrue();
        assertThat(counterOfRetrievedNumberEvents).isEqualTo(numEventsToPublish);

        List<MyMixedEvent> lastMixedReceivedEvents = new ArrayList<>();
        int counterOfRetrievedMixedEvents = 0;
        for (MyMixedEventSubscriber mySubscriber : myMixedEventSubscribers) {
            lastMixedReceivedEvents.add(mySubscriber.lastReceivedEvent);
            counterOfRetrievedMixedEvents += mySubscriber.counterOfRetrievedEvents;
        }
        assertThat(lastMixedReceivedEvents.contains(lastMixedEvent)).isTrue();
        assertThat(counterOfRetrievedMixedEvents).isEqualTo(numEventsToPublish);

        // assert
        for (MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber : mySubscribersEachInItsOwnGroup) {
            assertThat(mySubscriber.lastReceivedEvent).isEqualTo(lastNumberEvent);
            assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
        }

        // LocalDateTime end = LocalDateTime.now();
        // System.out.println("test duration was " + (Duration.between(start,end).toMillis()) + " ms");
    }
}