package p.studio.utilities.events;

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

final class TypedEventBusTest {
    @Test
    void publishesTypedEventsToSubscribers() {
        final TypedEventBus bus = new TypedEventBus();
        final List<String> received = new CopyOnWriteArrayList<>();

        bus.subscribe(TestEvent.class, event -> received.add(event.value()));
        bus.publish(new TestEvent("hello"));

        assertEquals(List.of("hello"), received);
    }

    @Test
    void unsubscribedListenersStopReceivingEvents() {
        final TypedEventBus bus = new TypedEventBus();
        final AtomicInteger calls = new AtomicInteger();

        final EventSubscription subscription = bus.subscribe(TestEvent.class, event -> calls.incrementAndGet());
        subscription.unsubscribe();

        bus.publish(new TestEvent("hello"));

        assertEquals(0, calls.get());
    }

    @Test
    void publishAndSubscriptionManagementAreThreadSafe() throws Exception {
        final TypedEventBus bus = new TypedEventBus();
        final AtomicInteger received = new AtomicInteger();
        final int publisherCount = 4;
        final int eventsPerPublisher = 250;
        final int subscriptionCount = 12;

        final CountDownLatch ready = new CountDownLatch(publisherCount + subscriptionCount);
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch done = new CountDownLatch(publisherCount + subscriptionCount);
        try (final var executor = Executors.newFixedThreadPool(publisherCount + subscriptionCount)) {

            for (int i = 0; i < subscriptionCount; i++) {
                executor.submit(() -> {
                    ready.countDown();
                    await(start);

                    final EventSubscription subscription =
                            bus.subscribe(TestEvent.class, event -> received.incrementAndGet());
                    bus.publish(new TestEvent("subscription-ready"));
                    subscription.unsubscribe();
                    done.countDown();
                });
            }

            for (int i = 0; i < publisherCount; i++) {
                executor.submit(() -> {
                    ready.countDown();
                    await(start);

                    for (int j = 0; j < eventsPerPublisher; j++) {
                        bus.publish(new TestEvent("payload-" + j));
                    }
                    done.countDown();
                });
            }

            assertTrue(ready.await(5, TimeUnit.SECONDS));
            start.countDown();
            assertTrue(done.await(5, TimeUnit.SECONDS));
            executor.shutdownNow();
            assertTrue(received.get() >= 0);
        }
    }

    private static void await(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException interruptedException) {
            Thread.currentThread().interrupt();
            throw new AssertionError(interruptedException);
        }
    }

    private record TestEvent(String value) implements SimpleEvent {
    }
}
