package com.livingworld.events;

import static org.junit.jupiter.api.Assertions.*;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

/**
 * Tests for {@link LivingWorldEventBus}.
 */
public class LivingWorldEventBusTest {

    // ------------------------------------------------------------------
    // Registration tests
    // ------------------------------------------------------------------

    @Test
    void registerAddsListenerForEventType() {
        LivingWorldEventBus bus = new LivingWorldEventBus();
        LivingWorldEventListener listener = event -> {};

        assertDoesNotThrow(() -> bus.register("weather_change", listener));
    }

    @Test
    void registerPreservesRegistrationOrder() {
        LivingWorldEventBus bus = new LivingWorldEventBus();
        AtomicBoolean firstCalled = new AtomicBoolean(false);
        AtomicBoolean secondCalled = new AtomicBoolean(false);

        LivingWorldEventListener first = event -> firstCalled.set(true);
        LivingWorldEventListener second = event -> secondCalled.set(true);

        bus.register("weather_change", first);
        bus.register("weather_change", second);

        BaseLivingWorldEvent event = new BaseLivingWorldEvent("weather_change", 0L, "core");
        publishEvent(bus, "weather_change", event);

        assertTrue(firstCalled.get());
        assertTrue(secondCalled.get());
    }

    @Test
    void registerBlankEventTypeThrows() {
        LivingWorldEventBus bus = new LivingWorldEventBus();

        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            bus.register(" ", null);
        });

        assertEquals("eventType must not be null or blank", exception.getMessage());
    }

    @Test
    void registerNullListenerThrows() {
        LivingWorldEventBus bus = new LivingWorldEventBus();

        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            bus.register("weather_change", null);
        });

        assertEquals("listener must not be null", exception.getMessage());
    }

    // ------------------------------------------------------------------
    // Publishing tests
    // ------------------------------------------------------------------

    @Test
    void publishDispatchesToRegisteredListeners() {
        LivingWorldEventBus bus = new LivingWorldEventBus();
        AtomicBoolean listenerCalled = new AtomicBoolean(false);

        bus.register("weather_change", event -> listenerCalled.set(true));

        BaseLivingWorldEvent event = new BaseLivingWorldEvent(
                "weather_change", 100L, "core"
        );

        publishEvent(bus, "weather_change", event);

        assertTrue(listenerCalled.get());
    }

    @Test
    void publishUnknownEventTypeDoesNotCrash() {
        LivingWorldEventBus bus = new LivingWorldEventBus();
        BaseLivingWorldEvent event = new BaseLivingWorldEvent("unknown_type_123", 100L, "core");

        assertDoesNotThrow(() -> publishEvent(bus, "unknown_type_123", event));
    }

    @Test
    void publishNullEventThrows() {
        LivingWorldEventBus bus = new LivingWorldEventBus();

        assertThrowsWithMessage(() -> { try { publishEvent(bus, "weather_change", null); } catch (RuntimeException e) { throw e; } }, 
                IllegalArgumentException.class, "event must not be null");
    }

    // ------------------------------------------------------------------
    // Metrics tests
    // ------------------------------------------------------------------

    @Test
    void getPublishedEventCountIncrementsOnPublish() {
        LivingWorldEventBus bus = new LivingWorldEventBus();

        BaseLivingWorldEvent event1 = new BaseLivingWorldEvent("weather_change", 100L, "core");
        bus.publish(event1);

        assertEquals(1, bus.getPublishedEventCount());

        // Clear suppression to allow publishing events with different ticks
        bus.clearSuppressionForCurrentTick();

        BaseLivingWorldEvent event2 = new BaseLivingWorldEvent("soil_moisture", 200L, "core");
        bus.publish(event2);

        assertEquals(2, bus.getPublishedEventCount());
    }

    @Test
    void multipleEventsAtSameTickAreAllDelivered() {
        LivingWorldEventBus bus = new LivingWorldEventBus();
        List<String> delivered = new ArrayList<>();
        bus.register("soil", event -> delivered.add(event.eventType()));
        bus.register("water", event -> delivered.add(event.eventType()));

        bus.publish(new BaseLivingWorldEvent("soil", 10L, "soil"));
        bus.publish(new BaseLivingWorldEvent("water", 10L, "water"));

        assertEquals(List.of("soil", "water"), delivered);
        assertEquals(2, bus.getPublishedEventCount());
    }

    @Test
    void recursivelyPublishedEventIsQueuedAndDelivered() {
        LivingWorldEventBus bus = new LivingWorldEventBus();
        List<String> delivered = new ArrayList<>();
        bus.register("first", event -> {
            delivered.add("first");
            bus.publish(new BaseLivingWorldEvent("second", event.simulationTick(), "first"));
        });
        bus.register("second", event -> delivered.add("second"));

        bus.publish(new BaseLivingWorldEvent("first", 10L, "test"));

        assertEquals(List.of("first", "second"), delivered);
        assertEquals(2, bus.getPublishedEventCount());
    }

    @Test
    void getListenerCountReturnsCorrectNumberOfListeners() {
        LivingWorldEventBus bus = new LivingWorldEventBus();

        bus.register("weather_change", event -> {});
        bus.register("weather_change", event -> {});
        bus.register("pollution_level", event -> {});

        assertEquals(2, bus.getListenerCount("weather_change"));
        assertEquals(1, bus.getListenerCount("pollution_level"));
        assertEquals(0, bus.getListenerCount("unknown_event"));
    }

    // ------------------------------------------------------------------
    // Helper methods (kept in test file for simplicity)
    // ------------------------------------------------------------------

    private void publishEvent(LivingWorldEventBus bus, String eventType, LivingWorldEvent event) {
        // Use reflection to call the package-private publish method
        try {
            java.lang.reflect.Method method = LivingWorldEventBus.class.getDeclaredMethod(
                "publish", LivingWorldEvent.class
            );
            method.setAccessible(true);
            if (event == null) {
                method.invoke(bus, new Object[]{null});
            } else {
                method.invoke(bus, event);
            }
        } catch (java.lang.reflect.InvocationTargetException e) {
            // Unwrap the target exception for proper test assertions
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            } else if (cause != null) {
                throw new RuntimeException(cause);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void assertThrowsWithMessage(Runnable runnable, Class<? extends Throwable> expectedType, String expectedMessage) {
        try {
            runnable.run();
            fail("Expected " + expectedType.getSimpleName() + " but no exception was thrown");
        } catch (AssertionError e) {
            throw e;
        } catch (Throwable t) {
            if (expectedType.isInstance(t)) {
                assertEquals(expectedMessage, t.getMessage());
            } else {
                throw new AssertionError("Expected " + expectedType.getSimpleName() + " but got " + t.getClass().getSimpleName() + ": " + t.getMessage(), t);
            }
        }
    }
}
