package one.edee.mcp.jdwp;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Unit tests for {@link EventHistory}, the bounded ring buffer that backs {@code jdwp_get_events}.
 * These tests are pure — no JDI involvement — and exercise insertion, eviction at the size limit,
 * recent-N retrieval ordering, and clearing semantics.
 */
class EventHistoryTest {

	private static final int MAX_EVENTS = 500;

	@Test
	@DisplayName("recording an event makes it retrievable via getRecent")
	void shouldRecordAndRetrieveEvent() {
		EventHistory history = new EventHistory();

		history.record(new EventHistory.DebugEvent("BREAKPOINT", "hit at Foo:42"));

		List<EventHistory.DebugEvent> recent = history.getRecent(10);
		assertThat(recent).hasSize(1);
		assertThat(recent.get(0).type()).isEqualTo("BREAKPOINT");
		assertThat(recent.get(0).summary()).isEqualTo("hit at Foo:42");
	}

	@Test
	@DisplayName("getRecent returns events in insertion order")
	void shouldReturnEventsInInsertionOrder() {
		EventHistory history = new EventHistory();
		history.record(new EventHistory.DebugEvent("BREAKPOINT", "first"));
		history.record(new EventHistory.DebugEvent("STEP", "second"));
		history.record(new EventHistory.DebugEvent("EXCEPTION", "third"));

		List<EventHistory.DebugEvent> recent = history.getRecent(10);

		assertThat(recent).extracting(EventHistory.DebugEvent::summary)
			.containsExactly("first", "second", "third");
	}

	@Test
	@DisplayName("getRecent honours the count parameter and returns the latest N")
	void shouldReturnLatestNEvents() {
		EventHistory history = new EventHistory();
		for (int i = 0; i < 10; i++) {
			history.record(new EventHistory.DebugEvent("BREAKPOINT", "event-" + i));
		}

		List<EventHistory.DebugEvent> recent = history.getRecent(3);

		assertThat(recent).extracting(EventHistory.DebugEvent::summary)
			.containsExactly("event-7", "event-8", "event-9");
	}

	@Test
	@DisplayName("ring buffer evicts oldest entries beyond the MAX_EVENTS cap")
	void shouldEvictOldestWhenExceedingMaxSize() {
		EventHistory history = new EventHistory();
		for (int i = 0; i < MAX_EVENTS + 10; i++) {
			history.record(new EventHistory.DebugEvent("BREAKPOINT", "event-" + i));
		}

		assertThat(history.size()).isEqualTo(MAX_EVENTS);

		List<EventHistory.DebugEvent> recent = history.getRecent(MAX_EVENTS);
		assertThat(recent).hasSize(MAX_EVENTS);
		// First retained entry should be the 10th (events 0..9 were evicted)
		assertThat(recent.get(0).summary()).isEqualTo("event-10");
		assertThat(recent.get(MAX_EVENTS - 1).summary()).isEqualTo("event-" + (MAX_EVENTS + 9));
	}

	@Test
	@DisplayName("clear empties the buffer")
	void shouldClear() {
		EventHistory history = new EventHistory();
		history.record(new EventHistory.DebugEvent("STEP", "x"));
		history.record(new EventHistory.DebugEvent("STEP", "y"));
		assertThat(history.size()).isEqualTo(2);

		history.clear();

		assertThat(history.size()).isZero();
		assertThat(history.getRecent(10)).isEmpty();
	}

	@Test
	@DisplayName("getRecent on an empty history returns an empty list")
	void shouldReturnEmptyListOnEmptyHistory() {
		EventHistory history = new EventHistory();
		assertThat(history.getRecent(20)).isEmpty();
	}

	@Test
	@DisplayName("getRecent caps to actual size when count exceeds buffer")
	void shouldClampCountToBufferSize() {
		EventHistory history = new EventHistory();
		history.record(new EventHistory.DebugEvent("BREAKPOINT", "only"));

		assertThat(history.getRecent(100)).hasSize(1);
	}

	@Test
	@DisplayName("DebugEvent two-arg constructor sets timestamp and empty details")
	void shouldDefaultDetailsToEmptyMap() {
		EventHistory.DebugEvent event = new EventHistory.DebugEvent("LOGPOINT", "x = 5");

		assertThat(event.type()).isEqualTo("LOGPOINT");
		assertThat(event.summary()).isEqualTo("x = 5");
		assertThat(event.details()).isEmpty();
		assertThat(event.timestamp()).isNotNull();
	}

	@Test
	@DisplayName("DebugEvent three-arg constructor preserves the details map")
	void shouldPreserveDetailsMap() {
		Map<String, String> details = Map.of("breakpointId", "5", "thread", "main");
		EventHistory.DebugEvent event = new EventHistory.DebugEvent("BREAKPOINT", "hit", details);

		assertThat(event.details()).containsExactlyInAnyOrderEntriesOf(details);
	}
}
