package domain

import (
	"fmt"
	"testing"
	"time"
)

type mockTimeProvider struct {
	now time.Time
}

func (m *mockTimeProvider) Now() time.Time {
	return m.now
}

func TestCorrelationService_Match(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send Apache event (should be buffered, not emitted)
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}

	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 results (buffered), got %d", len(results))
	}

	// Send matching Network event within window
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now.Add(500 * time.Millisecond),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc"},
	}

	// This should match and return correlated log
	results = svc.ProcessEvent(networkEvent)
	if len(results) != 1 {
		t.Errorf("expected 1 result (correlated), got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result")
	}
}

func TestCorrelationService_NoMatch_DifferentIP(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}

	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.2", // Different IP
		SrcPort:   8080,
	}

	svc.ProcessEvent(apacheEvent)
	results := svc.ProcessEvent(networkEvent)

	if len(results) != 0 {
		t.Errorf("expected 0 results (different IP), got %d", len(results))
	}
}

func TestCorrelationService_NoMatch_TimeWindowExceeded(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}

	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now.Add(2 * time.Second), // Outside window
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}

	svc.ProcessEvent(apacheEvent)
	results := svc.ProcessEvent(networkEvent)

	if len(results) != 0 {
		t.Errorf("expected 0 results (time window exceeded), got %d", len(results))
	}
}

func TestCorrelationService_Flush(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		ApacheEmitDelayMs:  0, // Zero delay = immediate emission (backward compatibility)
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}

	// A est émis immédiatement quand ApacheAlwaysEmit=true et ApacheEmitDelayMs=0
	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 1 {
		t.Fatalf("expected 1 immediate orphan event, got %d", len(results))
	}

	flushed := svc.Flush()
	if len(flushed) != 0 {
		t.Errorf("expected 0 flushed events, got %d", len(flushed))
	}
}

func TestCorrelationService_GetBufferSizes(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Empty buffers
	a, b := svc.GetBufferSizes()
	if a != 0 || b != 0 {
		t.Errorf("expected empty buffers, got A=%d, B=%d", a, b)
	}

	// Add event to buffer A
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	svc.ProcessEvent(apacheEvent)

	a, b = svc.GetBufferSizes()
	if a != 1 || b != 0 {
		t.Errorf("expected A=1, B=0, got A=%d, B=%d", a, b)
	}
}

func TestCorrelationService_FlushWithEvents(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		NetworkEmit:        true,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}
	svc := NewCorrelationService(config, timeProvider)

	// Add events directly to buffers for testing Flush
	keyA := "192.168.1.1:8080"
	keyB := "192.168.1.2:9090"

	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.2",
		SrcPort:   9090,
	}

	elemA := svc.bufferA.events.PushBack(apacheEvent)
	svc.pendingA[keyA] = append(svc.pendingA[keyA], elemA)

	elemB := svc.bufferB.events.PushBack(networkEvent)
	svc.pendingB[keyB] = append(svc.pendingB[keyB], elemB)
	svc.networkTTLs[elemB] = now.Add(time.Duration(svc.config.NetworkTTLS) * time.Second)

	flushed := svc.Flush()
	if len(flushed) != 1 {
		t.Errorf("expected 1 flushed event (A only), got %d", len(flushed))
	}
	if len(flushed) == 1 && flushed[0].OrphanSide != "A" {
		t.Errorf("expected orphan side A, got %s", flushed[0].OrphanSide)
	}

	// Verify buffers are cleared
	a, b := svc.GetBufferSizes()
	if a != 0 || b != 0 {
		t.Errorf("expected empty buffers after flush, got A=%d, B=%d", a, b)
	}
}

func TestCorrelationService_BufferOverflow(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MaxHTTPBufferSize:  2,
		MaxNetworkBufferSize: 2,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Fill buffer A
	for i := 0; i < 2; i++ {
		event := &NormalizedEvent{
			Source:    SourceA,
			Timestamp: now,
			SrcIP:     "192.168.1.1",
			SrcPort:   8080 + i,
		}
		svc.ProcessEvent(event)
	}

	// Buffer full, next event should be dropped (not emitted since ApacheAlwaysEmit=false)
	overflowEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   9999,
	}
	results := svc.ProcessEvent(overflowEvent)
	if len(results) != 0 {
		t.Errorf("expected 0 results on buffer overflow, got %d", len(results))
	}
}

func TestCorrelationService_DefaultConfig(t *testing.T) {
	timeProvider := &RealTimeProvider{}

	// Test with zero config - should use defaults
	config := CorrelationConfig{}
	svc := NewCorrelationService(config, timeProvider)

	if svc.config.MaxHTTPBufferSize != DefaultMaxHTTPBufferSize {
		t.Errorf("expected MaxHTTPBufferSize %d, got %d", DefaultMaxHTTPBufferSize, svc.config.MaxHTTPBufferSize)
	}
	if svc.config.MaxNetworkBufferSize != DefaultMaxNetworkBufferSize {
		t.Errorf("expected MaxNetworkBufferSize %d, got %d", DefaultMaxNetworkBufferSize, svc.config.MaxNetworkBufferSize)
	}
	if svc.config.TimeWindow != DefaultTimeWindow {
		t.Errorf("expected TimeWindow %v, got %v", DefaultTimeWindow, svc.config.TimeWindow)
	}
	if svc.config.NetworkTTLS != DefaultNetworkTTLS {
		t.Errorf("expected NetworkTTLS %d, got %d", DefaultNetworkTTLS, svc.config.NetworkTTLS)
	}
	if svc.config.MatchingMode != MatchingModeOneToMany {
		t.Errorf("expected MatchingMode %s, got %s", MatchingModeOneToMany, svc.config.MatchingMode)
	}
}

func TestCorrelationService_NilTimeProvider(t *testing.T) {
	config := CorrelationConfig{
		TimeWindow: time.Second,
	}

	// Should not panic with nil time provider
	svc := NewCorrelationService(config, nil)
	if svc == nil {
		t.Error("expected non-nil service")
	}
}

func TestCorrelationService_DifferentSourceTypes(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send B first, then A - should still match
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results := svc.ProcessEvent(networkEvent)
	if len(results) != 0 {
		t.Errorf("expected 0 results (buffered B), got %d", len(results))
	}

	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now.Add(500 * time.Millisecond),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results = svc.ProcessEvent(apacheEvent)
	if len(results) < 1 {
		t.Errorf("expected at least 1 result (correlated), got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result")
	}
}

func TestCorrelationService_NetworkEmitTrue_DoesNotEmitBAlone(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        true,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "10.10.10.10",
		SrcPort:   5555,
	}

	results := svc.ProcessEvent(networkEvent)
	if len(results) != 0 {
		t.Errorf("expected 0 immediate results for orphan B, got %d", len(results))
	}

	flushed := svc.Flush()
	if len(flushed) != 0 {
		t.Errorf("expected 0 flushed orphan B events, got %d", len(flushed))
	}
}

func TestCorrelationService_OneToMany_KeepAlive(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany, // Keep-Alive mode
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send B event first (network)
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}
	results := svc.ProcessEvent(networkEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 results (B buffered), got %d", len(results))
	}

	// Send first A event (Apache) - should correlate with B
	apacheEvent1 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now.Add(500 * time.Millisecond),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET", "path": "/api/first"},
	}
	results = svc.ProcessEvent(apacheEvent1)
	if len(results) != 1 {
		t.Errorf("expected 1 correlated result for first A, got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result for first A")
	}

	// Send second A event (same connection, Keep-Alive) - should also correlate with same B
	apacheEvent2 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now.Add(1 * time.Second),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET", "path": "/api/second"},
	}
	results = svc.ProcessEvent(apacheEvent2)
	if len(results) != 1 {
		t.Errorf("expected 1 correlated result for second A (Keep-Alive), got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result for second A (Keep-Alive)")
	}

	// Verify B is still in buffer (Keep-Alive)
	a, b := svc.GetBufferSizes()
	if a != 0 {
		t.Errorf("expected A buffer empty, got %d", a)
	}
	if b != 1 {
		t.Errorf("expected B buffer size 1 (Keep-Alive), got %d", b)
	}
}

func TestCorrelationService_OneToOne_ConsumeB(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToOne, // Consume B after match
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send B event first
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}
	svc.ProcessEvent(networkEvent)

	// Send first A event - should correlate and consume B
	apacheEvent1 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now.Add(500 * time.Millisecond),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results := svc.ProcessEvent(apacheEvent1)
	if len(results) != 1 {
		t.Fatalf("expected 1 correlated result, got %d", len(results))
	}

	// Send second A event - should NOT correlate (B was consumed)
	apacheEvent2 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now.Add(1 * time.Second),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results = svc.ProcessEvent(apacheEvent2)
	if len(results) != 0 {
		t.Errorf("expected 0 results (B consumed), got %d", len(results))
	}

	// Verify both buffers are empty
	a, b := svc.GetBufferSizes()
	if a != 1 {
		t.Errorf("expected A buffer size 1 (second A buffered), got %d", a)
	}
	if b != 0 {
		t.Errorf("expected B buffer empty (consumed), got %d", b)
	}
}

func TestCorrelationService_NetworkTTL_ResetOnMatch(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second, // 5 seconds time window for correlation
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        10, // 10 seconds TTL for B events
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send B event
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	svc.ProcessEvent(networkEvent)

	// Verify B is in buffer
	_, b := svc.GetBufferSizes()
	if b != 1 {
		t.Fatalf("expected B in buffer, got %d", b)
	}

	// Advance time by 3 seconds (before TTL expires)
	timeProvider.now = now.Add(3 * time.Second)

	// Send A event with timestamp within time window of B
	// A's timestamp is t=3s, B's timestamp is t=0s, diff = 3s < 5s (time_window)
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: timeProvider.now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 1 {
		t.Fatalf("expected 1 correlated result, got %d", len(results))
	}

	// B should still be in buffer (TTL reset)
	_, b = svc.GetBufferSizes()
	if b != 1 {
		t.Errorf("expected B still in buffer after TTL reset, got %d", b)
	}

	// Advance time by 7 more seconds (total 10s from start, 7s from last match)
	timeProvider.now = now.Add(10 * time.Second)

	// B should still be alive (TTL was reset to 10s from t=3s, so expires at t=13s)
	svc.cleanExpired()
	_, b = svc.GetBufferSizes()
	if b != 1 {
		t.Errorf("expected B still alive after TTL reset, got %d", b)
	}

	// Advance time past the reset TTL (t=14s > t=13s)
	timeProvider.now = now.Add(14 * time.Second)
	svc.cleanExpired()
	_, b = svc.GetBufferSizes()
	if b != 0 {
		t.Errorf("expected B expired after reset TTL, got %d", b)
	}
}

// TestCorrelationService_KeepAlive_TTLNotBasedOnEventTimestamp tests the critical bug fix:
// B events must NOT be evicted based on their original timestamp when TTL is reset.
// This is essential for HTTP Keep-Alive where multiple A events correlate with the same B.
func TestCorrelationService_KeepAlive_TTLNotBasedOnEventTimestamp(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        10, // 10 seconds TTL
	}

	svc := NewCorrelationService(config, timeProvider)

	// T0: B event arrives (network log with old timestamp)
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now, // t=0
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}
	svc.ProcessEvent(networkEvent)

	// T0+1s: First A event - correlates with B, TTL reset to t=11s
	timeProvider.now = now.Add(1 * time.Second)
	apacheEvent1 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: timeProvider.now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results := svc.ProcessEvent(apacheEvent1)
	if len(results) != 1 || results[0].Correlated == 0 {
		t.Fatalf("expected 1 correlated result, got %d", len(results))
	}

	// T0+2s: Second A event - should also correlate (Keep-Alive), TTL reset to t=12s
	timeProvider.now = now.Add(2 * time.Second)
	apacheEvent2 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: timeProvider.now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
	}
	results = svc.ProcessEvent(apacheEvent2)
	if len(results) != 1 || results[0].Correlated == 0 {
		t.Fatalf("expected 1 correlated result (Keep-Alive), got %d", len(results))
	}

	// T0+11s: B should still be alive (TTL was reset to t=12s)
	// The bug would evict B here because B.timestamp (t=0) < cutoff (t=11s-10s=t=1s)
	timeProvider.now = now.Add(11 * time.Second)
	svc.cleanExpired()
	_, b := svc.GetBufferSizes()
	if b != 1 {
		t.Errorf("BUG: B was evicted based on original timestamp! Expected B alive (TTL reset to t=12s), got buffer size %d", b)
	}

	// T0+13s: Now B should be expired (TTL reset was at t=2s, expires at t=12s)
	timeProvider.now = now.Add(13 * time.Second)
	svc.cleanExpired()
	_, b = svc.GetBufferSizes()
	if b != 0 {
		t.Errorf("expected B expired at t=13s (TTL was t=12s), got buffer size %d", b)
	}
}

// TestCorrelationService_KeepAlive_LongSession tests that B events are NOT evicted
// based on their original timestamp when TTL is reset on each correlation.
// This is the core bug fix for Keep-Alive support.
//
// Scenario: B arrives at t=0 with timestamp t=0. Multiple A events arrive
// with timestamps within the correlation window of B (t=0).
// The bug would evict B when its original timestamp (t=0) becomes "old" compared
// to "now", but the fix uses TTL map only, so B stays alive as long as TTL is reset.
func TestCorrelationService_KeepAlive_LongSession(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second, // Window for correlation
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        120, // 120 seconds TTL for long sessions
	}

	svc := NewCorrelationService(config, timeProvider)

	// T0: Network event (TLS handshake) - timestamp = now
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123", "ja4": "def456"},
	}
	svc.ProcessEvent(networkEvent)

	// Simulate 6 HTTP requests over 25 seconds (Keep-Alive)
	// All A timestamps are within the 5s correlation window of B (t=0)
	// A timestamps: t=0.5, 1, 1.5, 2, 2.5, 3 (all within 5s of B's t=0)
	for i := 1; i <= 6; i++ {
		// Advance "now" by 5s between each request
		timeProvider.now = now.Add(time.Duration(i*5) * time.Second)
		
		// A timestamp stays within the correlation window of B (t=0)
		// This simulates A events whose timestamps are close to B's timestamp
		apacheEvent := &NormalizedEvent{
			Source:    SourceA,
			Timestamp: now.Add(time.Duration(i) * 500 * time.Millisecond), // t=0.5, 1, 1.5, 2, 2.5, 3
			SrcIP:     "192.168.1.1",
			SrcPort:   8080,
			Raw:       map[string]any{"method": "GET", "path": fmt.Sprintf("/api/%d", i)},
		}
		results := svc.ProcessEvent(apacheEvent)
		if len(results) != 1 || results[0].Correlated == 0 {
			t.Errorf("Request %d at t=%ds (A timestamp t=%v): expected correlation, got %d results", 
				i, i*5, now.Add(time.Duration(i)*500*time.Millisecond), len(results))
		}
	}

	// After 25 seconds of real time, B should still be alive
	// because TTL was reset at each correlation (last reset at t=30s, expires at t=150s)
	// The bug would have evicted B at t=30s because B.timestamp (t=0) < cutoff (t=30s-120s)
	timeProvider.now = now.Add(25 * time.Second)
	svc.cleanExpired()
	_, b := svc.GetBufferSizes()
	if b != 1 {
		t.Errorf("BUG: B was evicted based on original timestamp at t=25s! Expected B alive (TTL reset), got buffer size %d", b)
	}

	// T0+155s: B should expire (last TTL reset was at t=30s + 120s TTL = t=150s)
	timeProvider.now = now.Add(155 * time.Second)
	svc.cleanExpired()
	_, b = svc.GetBufferSizes()
	if b != 0 {
		t.Errorf("expected B expired at t=155s (TTL was reset at t=30s + 120s = t=150s), got buffer size %d", b)
	}
}

// TestCorrelationService_ALateThanB_WithinTimeWindow tests that A events arriving after B
// within the time window are correctly correlated.
func TestCorrelationService_ALateThanB_WithinTimeWindow(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
	}

	svc := NewCorrelationService(config, timeProvider)

	// B arrives first at t=0
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}
	results := svc.ProcessEvent(networkEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 results (B buffered), got %d", len(results))
	}

	// A arrives later at t=2s (within time window of B)
	timeProvider.now = now.Add(2 * time.Second)
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: timeProvider.now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}
	results = svc.ProcessEvent(apacheEvent)
	if len(results) != 1 {
		t.Errorf("expected 1 correlated result, got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result")
	}
}

// TestCorrelationService_ALateThanB_AExpiredTooSoon tests that A events are not
// prematurely expired when B events arrive within the TimeWindow of A's timestamp.
// This test verifies that correlation is based on event timestamps, not system time.
func TestCorrelationService_ALateThanB_AExpiredTooSoon(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         10 * time.Second, // Larger window to allow correlation
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        30, // 30 seconds TTL for B
	}

	svc := NewCorrelationService(config, timeProvider)

	// A arrives at t=0
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}
	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 results (A buffered), got %d", len(results))
	}

	// Advance time to t=6s (A is 6s old, still within TimeWindow=10s)
	timeProvider.now = now.Add(6 * time.Second)
	svc.cleanExpired()

	a, _ := svc.GetBufferSizes()
	if a != 1 {
		t.Errorf("expected A still in buffer at t=6s, got A=%d", a)
	}

	// B arrives at t=6s with timestamp t=5s (within TimeWindow of A at t=0)
	// A.timestamp=t=0, B.timestamp=t=5, diff=5s < TimeWindow=10s -> should match
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now.Add(5 * time.Second),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}
	results = svc.ProcessEvent(networkEvent)
	if len(results) != 1 {
		t.Errorf("expected 1 correlated result, got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result")
	}
}

// TestCorrelationService_Flush_CorrelatesRemainingEvents tests that Flush()
// correlates remaining A and B events before emitting orphans.
func TestCorrelationService_Flush_CorrelatesRemainingEvents(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second,
		ApacheAlwaysEmit:   true,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Add A event to buffer (ApacheAlwaysEmit=false would buffer, but we set true)
	// So we manually add to buffer for testing
	keyA := "192.168.1.1:8080"
	keyB := "192.168.1.1:8080"

	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now.Add(2 * time.Second),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}

	// Manually add to buffers (bypassing ProcessEvent logic)
	elemA := svc.bufferA.events.PushBack(apacheEvent)
	svc.pendingA[keyA] = append(svc.pendingA[keyA], elemA)

	elemB := svc.bufferB.events.PushBack(networkEvent)
	svc.pendingB[keyB] = append(svc.pendingB[keyB], elemB)
	svc.networkTTLs[elemB] = now.Add(time.Duration(svc.config.NetworkTTLS) * time.Second)

	// Flush should correlate A and B, not emit as orphans
	flushed := svc.Flush()
	if len(flushed) != 1 {
		t.Errorf("expected 1 flushed correlated result, got %d", len(flushed))
	} else if flushed[0].Correlated != 0 {
		// Good - it's correlated
	} else {
		t.Errorf("expected correlated result, got orphan side %s", flushed[0].OrphanSide)
	}

	// Verify buffers are cleared
	a, b := svc.GetBufferSizes()
	if a != 0 || b != 0 {
		t.Errorf("expected empty buffers after flush, got A=%d, B=%d", a, b)
	}
}

// TestCorrelationService_BufferFull_RotatesOldestA tests that when buffer A is full,
// the oldest A event is rotated (not immediate emission of new event).
func TestCorrelationService_BufferFull_RotatesOldestA(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second,
		ApacheAlwaysEmit:   false, // Must be false to buffer events
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  3,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Fill buffer to capacity
	for i := 0; i < 3; i++ {
		event := &NormalizedEvent{
			Source:    SourceA,
			Timestamp: now.Add(time.Duration(i) * time.Second),
			SrcIP:     "192.168.1.1",
			SrcPort:   8080 + i,
			Raw:       map[string]any{"method": "GET"},
		}
		results := svc.ProcessEvent(event)
		// With rotation, new events are buffered, oldest is rotated out
		// Rotation doesn't emit (only logs), so results should be 0
		if len(results) != 0 {
			t.Errorf("event %d: expected 0 results (buffered), got %d", i, len(results))
		}
	}

	a, _ := svc.GetBufferSizes()
	if a != 3 {
		t.Errorf("expected buffer A size 3, got %d", a)
	}

	// Add 4th event - should rotate oldest (port 8080) and buffer new one
	overflowEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now.Add(10 * time.Second),
		SrcIP:     "192.168.1.1",
		SrcPort:   9999,
		Raw:       map[string]any{"method": "POST"},
	}
	results := svc.ProcessEvent(overflowEvent)
	if len(results) != 0 {
		t.Errorf("expected 0 results on overflow (rotated), got %d", len(results))
	}

	// Buffer should still have 3 events (oldest rotated, newest added)
	a, _ = svc.GetBufferSizes()
	if a != 3 {
		t.Errorf("expected buffer A size 3 after rotation, got %d", a)
	}
}

// TestCorrelationService_CleanA_RespectsBTTL tests that cleaning A events
// respects the TTL of corresponding B events.
func TestCorrelationService_CleanA_RespectsBTTL(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         5 * time.Second,
		ApacheAlwaysEmit:   false,
		NetworkEmit:        false,
		MatchingMode:       MatchingModeOneToMany,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        30, // 30 seconds TTL for B
	}

	svc := NewCorrelationService(config, timeProvider)

	// Add B event at t=0
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc123"},
	}
	svc.ProcessEvent(networkEvent)

	// Add A event at t=1
	timeProvider.now = now.Add(1 * time.Second)
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: timeProvider.now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}
	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 1 || results[0].Correlated == 0 {
		t.Fatalf("expected 1 correlated result, got %d", len(results))
	}

	// Advance time to t=10s (A is 9s old, beyond TimeWindow from "now")
	// But B is still valid (TTL=30s, reset at t=1, expires at t=31)
	timeProvider.now = now.Add(10 * time.Second)
	svc.cleanExpired()

	// A should have been emitted during correlation, buffer should be empty for A
	// B should still be in buffer (TTL not expired)
	a, b := svc.GetBufferSizes()
	if a != 0 {
		t.Errorf("expected A buffer empty (emitted during correlation), got %d", a)
	}
	if b != 1 {
		t.Errorf("expected B still in buffer (TTL valid), got %d", b)
	}

	// Now test the case where A is buffered (no match yet) and B expires
	// Add new A at t=10
	apacheEvent2 := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: timeProvider.now,
		SrcIP:     "192.168.1.2",
		SrcPort:   9090,
		Raw:       map[string]any{"method": "GET"},
	}
	svc.ProcessEvent(apacheEvent2)

	// Advance time past B TTL (t=35s, B TTL expired at t=31s)
	timeProvider.now = now.Add(35 * time.Second)
	svc.cleanExpired()

	// B should be expired now
	a, b = svc.GetBufferSizes()
	if b != 0 {
		t.Errorf("expected B expired at t=35s, got %d", b)
	}

	// A should still be in buffer (not yet beyond its own TimeWindow from "now")
	// Actually, A at t=10 is 25s old at t=35, which is > TimeWindow (5s)
	// So A should also be cleaned
	a, b = svc.GetBufferSizes()
	if a != 0 {
		t.Errorf("expected A also expired at t=35s (25s old > 5s TimeWindow), got %d", a)
	}
}

func TestCorrelationService_ApacheEmitDelay_BArrivesDuringDelay(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		ApacheEmitDelayMs:  500, // 500ms delay
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send Apache event - should be added to pending orphans, NOT emitted immediately
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}

	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 immediate results (pending orphan), got %d", len(results))
	}

	// Advance time by 250ms (less than delay)
	timeProvider.now = now.Add(250 * time.Millisecond)

	// Send B event - should correlate with pending A
	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now.Add(100 * time.Millisecond),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"ja3": "abc"},
	}

	results = svc.ProcessEvent(networkEvent)
	if len(results) != 1 {
		t.Errorf("expected 1 correlated result, got %d", len(results))
	} else if results[0].Correlated == 0 {
		t.Error("expected correlated result")
	}

	// Verify no pending orphans remain
	if len(svc.pendingOrphans) != 0 {
		t.Errorf("expected pending orphans empty, got %d keys", len(svc.pendingOrphans))
	}
}

func TestCorrelationService_ApacheEmitDelay_NoBArrives(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		ApacheEmitDelayMs:  500, // 500ms delay
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send Apache event
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}

	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 immediate results, got %d", len(results))
	}

	// Advance time past delay (600ms > 500ms)
	timeProvider.now = now.Add(600 * time.Millisecond)

	// Send another event (unrelated) to trigger emitPendingOrphans
	unrelatedEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "10.0.0.1",
		SrcPort:   9999,
	}

	results = svc.ProcessEvent(unrelatedEvent)
	if len(results) != 1 {
		t.Fatalf("expected 1 orphan result, got %d", len(results))
	}
	if results[0].OrphanSide != "A" {
		t.Errorf("expected orphan side A, got %s", results[0].OrphanSide)
	}
}

func TestCorrelationService_ApacheEmitDelay_ZeroDelay(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		ApacheEmitDelayMs:  0, // Zero delay = immediate emission (backward compat)
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send Apache event - should emit immediately with zero delay
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}

	results := svc.ProcessEvent(apacheEvent)
	// With zero delay, should emit immediately (backward compatibility mode)
	if len(results) != 1 {
		t.Fatalf("expected 1 immediate result (zero delay mode), got %d", len(results))
	}
}

func TestCorrelationService_ApacheEmitDelay_MultipleA_SameKey(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		ApacheEmitDelayMs:  1000, // 1s delay
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send multiple A events with same key
	for i := 0; i < 3; i++ {
		apacheEvent := &NormalizedEvent{
			Source:    SourceA,
			Timestamp: now.Add(time.Duration(i) * time.Millisecond),
			SrcIP:     "192.168.1.1",
			SrcPort:   8080,
			Raw:       map[string]any{"method": "GET", "path": fmt.Sprintf("/api/%d", i)},
		}
		results := svc.ProcessEvent(apacheEvent)
		if len(results) != 0 {
			t.Fatalf("expected 0 immediate results for A%d, got %d", i, len(results))
		}
	}

	// Verify 3 pending orphans
	key := "192.168.1.1:8080"
	if len(svc.pendingOrphans[key]) != 3 {
		t.Errorf("expected 3 pending orphans, got %d", len(svc.pendingOrphans[key]))
	}

	// Advance time past delay
	timeProvider.now = now.Add(1100 * time.Millisecond)

	// Trigger emit with unrelated event
	unrelatedEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: now,
		SrcIP:     "10.0.0.1",
		SrcPort:   9999,
	}
	results := svc.ProcessEvent(unrelatedEvent)

	// Should emit all 3 orphans
	if len(results) != 3 {
		t.Errorf("expected 3 orphan results, got %d", len(results))
	}
}

func TestCorrelationService_ApacheEmitDelay_Flush(t *testing.T) {
	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &mockTimeProvider{now: now}

	config := CorrelationConfig{
		TimeWindow:         time.Second,
		ApacheAlwaysEmit:   true,
		ApacheEmitDelayMs:  5000, // 5s delay
		NetworkEmit:        false,
		MaxHTTPBufferSize:  DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
		NetworkTTLS:        DefaultNetworkTTLS,
		MatchingMode:       MatchingModeOneToMany,
	}

	svc := NewCorrelationService(config, timeProvider)

	// Send A event
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: now,
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}

	results := svc.ProcessEvent(apacheEvent)
	if len(results) != 0 {
		t.Fatalf("expected 0 immediate results, got %d", len(results))
	}

	// Flush immediately (should emit pending orphans regardless of delay)
	flushed := svc.Flush()
	if len(flushed) != 1 {
		t.Errorf("expected 1 flushed orphan, got %d", len(flushed))
	} else if flushed[0].OrphanSide != "A" {
		t.Errorf("expected orphan side A, got %s", flushed[0].OrphanSide)
	}
}


func TestCorrelationService_IncludeDestPorts_AllowedPort(t *testing.T) {
now := time.Now()
mt := &mockTimeProvider{now: now}
svc := NewCorrelationService(CorrelationConfig{
TimeWindow:       10 * time.Second,
ApacheAlwaysEmit: true,
MatchingMode:     MatchingModeOneToMany,
IncludeDestPorts: []int{80, 443},
}, mt)

// B event on allowed port 443
bEvent := &NormalizedEvent{
Source: SourceB, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 443,
}
results := svc.ProcessEvent(bEvent)
if len(results) != 0 {
t.Fatalf("expected B buffered (no match yet), got %d results", len(results))
}

// A event on same key, allowed port
aEvent := &NormalizedEvent{
Source: SourceA, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 443,
}
results = svc.ProcessEvent(aEvent)
if len(results) != 1 {
t.Fatalf("expected 1 correlation, got %d", len(results))
}
if results[0].Correlated == 0 {
t.Error("expected correlated=true")
}
}

func TestCorrelationService_IncludeDestPorts_FilteredPort(t *testing.T) {
now := time.Now()
mt := &mockTimeProvider{now: now}
svc := NewCorrelationService(CorrelationConfig{
TimeWindow:       10 * time.Second,
ApacheAlwaysEmit: true,
MatchingMode:     MatchingModeOneToMany,
IncludeDestPorts: []int{80, 443},
}, mt)

// A event on port 22 (not in allow-list): must be emitted as uncorrelated
aEvent := &NormalizedEvent{
Source: SourceA, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 22,
}
results := svc.ProcessEvent(aEvent)
if len(results) != 1 {
t.Fatalf("expected 1 result (orphan A, dest port filtered), got %d", len(results))
}
if results[0].Correlated != 0 {
t.Errorf("expected Correlated=false for dest-port-filtered A event")
}
if results[0].OrphanSide != "A" {
t.Errorf("expected OrphanSide=A, got %q", results[0].OrphanSide)
}

// B event on port 22 (not in allow-list): no emission
bEvent := &NormalizedEvent{
Source: SourceB, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 22,
}
results = svc.ProcessEvent(bEvent)
if len(results) != 0 {
t.Fatalf("expected 0 results (B filtered), got %d", len(results))
}

// Flush should return nothing (nothing buffered)
flushed := svc.Flush()
if len(flushed) != 0 {
t.Errorf("expected 0 flushed events, got %d", len(flushed))
}
}

// TestCorrelationService_IncludeDestPorts_FilteredPort_NoAlwaysEmit verifies that
// when ApacheAlwaysEmit is false, A events on filtered ports are silently dropped.
func TestCorrelationService_IncludeDestPorts_FilteredPort_NoAlwaysEmit(t *testing.T) {
now := time.Now()
mt := &mockTimeProvider{now: now}
svc := NewCorrelationService(CorrelationConfig{
TimeWindow:       10 * time.Second,
ApacheAlwaysEmit: false,
MatchingMode:     MatchingModeOneToMany,
IncludeDestPorts: []int{80, 443},
}, mt)

// A event on port 22 (not in allow-list, ApacheAlwaysEmit=false): must be dropped
results := svc.ProcessEvent(&NormalizedEvent{
Source: SourceA, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 22,
})
if len(results) != 0 {
t.Fatalf("expected 0 results (ApacheAlwaysEmit=false), got %d", len(results))
}
}

func TestCorrelationService_IncludeDestPorts_EmptyAllowsAll(t *testing.T) {
now := time.Now()
mt := &mockTimeProvider{now: now}
// No IncludeDestPorts = all ports allowed
svc := NewCorrelationService(CorrelationConfig{
TimeWindow:       10 * time.Second,
ApacheAlwaysEmit: true,
MatchingMode:     MatchingModeOneToMany,
}, mt)

bEvent := &NormalizedEvent{
Source: SourceB, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 9999,
}
svc.ProcessEvent(bEvent)

aEvent := &NormalizedEvent{
Source: SourceA, Timestamp: now,
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 9999,
}
results := svc.ProcessEvent(aEvent)
if len(results) != 1 || results[0].Correlated == 0 {
t.Errorf("expected 1 correlation on any port when list is empty, got %d", len(results))
}
}

// TestCorrelationService_EmitPendingOrphans_MultipleExpiredSameKey tests that when multiple
// orphans for the same key expire simultaneously, each is emitted exactly once (bug fix).
func TestCorrelationService_EmitPendingOrphans_MultipleExpiredSameKey(t *testing.T) {
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
timeProvider := &mockTimeProvider{now: now}

config := CorrelationConfig{
TimeWindow:           10 * time.Second,
ApacheAlwaysEmit:     true,
ApacheEmitDelayMs:    500,
MatchingMode:         MatchingModeOneToMany,
MaxHTTPBufferSize:    DefaultMaxHTTPBufferSize,
MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
NetworkTTLS:          DefaultNetworkTTLS,
}
svc := NewCorrelationService(config, timeProvider)

// Send 3 A events for the same key — all go to pendingOrphans
for i := 0; i < 3; i++ {
a := &NormalizedEvent{
Source:    SourceA,
Timestamp: now.Add(time.Duration(i) * 100 * time.Millisecond),
SrcIP:     "10.0.0.1",
SrcPort:   4444,
}
results := svc.ProcessEvent(a)
if len(results) != 0 {
t.Fatalf("event %d: expected 0 results (pending orphan), got %d", i, len(results))
}
}

// Advance time past the emit delay — all 3 should expire
timeProvider.now = now.Add(600 * time.Millisecond)

// Trigger emitPendingOrphans via a new unrelated event
trigger := &NormalizedEvent{
Source:    SourceA,
Timestamp: timeProvider.now,
SrcIP:     "10.0.0.2",
SrcPort:   9999,
}
results := svc.ProcessEvent(trigger)

// Should get exactly 3 orphans (one per expired pending orphan), not more
orphanCount := 0
for _, r := range results {
if r.SrcIP == "10.0.0.1" {
orphanCount++
}
}
if orphanCount != 3 {
t.Errorf("expected exactly 3 orphan emissions for 10.0.0.1, got %d (check for slice aliasing bug)", orphanCount)
}

// pendingOrphans must be empty now — trigger again to confirm no ghost entries
timeProvider.now = timeProvider.now.Add(time.Second)
trigger2 := &NormalizedEvent{
Source:    SourceA,
Timestamp: timeProvider.now,
SrcIP:     "10.0.0.2",
SrcPort:   8888,
}
results2 := svc.ProcessEvent(trigger2)
for _, r := range results2 {
if r.SrcIP == "10.0.0.1" {
t.Errorf("ghost orphan re-emitted for 10.0.0.1 — slice aliasing bug still present")
}
}
}

// TestCorrelationService_BufferFull_RotatesOldestA_EmitsOrphan_AlwaysEmit tests that
// when buffer A is full with ApacheAlwaysEmit=true and immediate mode (delay=0),
// filling the buffer emits orphans immediately (so rotation is not triggered here),
// and tests the rotation path specifically with buffered events (ApacheAlwaysEmit=false
// fills buffer, then we switch expectation).
func TestCorrelationService_BufferFull_RotatesOldestA_EmitsOrphan_AlwaysEmit(t *testing.T) {
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
timeProvider := &mockTimeProvider{now: now}

// Use ApacheAlwaysEmit=false so events actually buffer (not emitted as orphans)
// Then verify that on rotation, the rotated event IS emitted when AlwaysEmit=true.
// We do this by first filling with AlwaysEmit=false, then changing config.
// Simplest approach: use a service where ApacheAlwaysEmit=true and events buffer
// (which requires them to have a matching B... but we want to test rotation).
//
// Strategy: fill buffer without B matches and ApacheAlwaysEmit=false so events are buffered.
// But we can't change config after creation. Instead, test the rotation function directly
// by using ApacheAlwaysEmit=true and ApacheEmitDelayMs=0 — in that mode events with no B
// are emitted immediately, so the buffer never fills. The rotation path is only hit when
// events are actually in the buffer (which requires a B match scenario or ApacheAlwaysEmit=false).
//
// Use ApacheAlwaysEmit=false to fill the buffer, then test the rotation log.

config := CorrelationConfig{
TimeWindow:           5 * time.Second,
ApacheAlwaysEmit:     false, // events buffer (no immediate emit)
MatchingMode:         MatchingModeOneToMany,
MaxHTTPBufferSize:    2,
MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
NetworkTTLS:          DefaultNetworkTTLS,
}
svc := NewCorrelationService(config, timeProvider)

// Fill buffer with 2 events
for i := 0; i < 2; i++ {
a := &NormalizedEvent{
Source: SourceA, Timestamp: now,
SrcIP: "10.1.1.1", SrcPort: 1000 + i,
}
svc.ProcessEvent(a)
}
aSize, _ := svc.GetBufferSizes()
if aSize != 2 {
t.Fatalf("expected buffer A=2, got %d", aSize)
}

// 3rd event triggers rotation — oldest A is removed from buffer
a3 := &NormalizedEvent{
Source: SourceA, Timestamp: now,
SrcIP: "10.1.1.1", SrcPort: 1002,
}
results := svc.ProcessEvent(a3)
// ApacheAlwaysEmit=false → rotated event is NOT emitted
if len(results) != 0 {
t.Errorf("ApacheAlwaysEmit=false: expected 0 results on rotation, got %d", len(results))
}
aSize, _ = svc.GetBufferSizes()
if aSize != 2 {
t.Errorf("expected buffer A=2 after rotation, got %d", aSize)
}
}

// TestCorrelationService_KeepAlive_PendingOrphanThenB_ThenA2 tests that in one_to_many mode,
// when A arrives first (→ pending orphan), then B arrives (→ correlated + B buffered),
// subsequent A2 on the same Keep-Alive connection still finds B in the buffer (bug fix).
func TestCorrelationService_KeepAlive_PendingOrphanThenB_ThenA2(t *testing.T) {
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
timeProvider := &mockTimeProvider{now: now}

config := CorrelationConfig{
TimeWindow:           10 * time.Second,
ApacheAlwaysEmit:     true,
ApacheEmitDelayMs:    500,
MatchingMode:         MatchingModeOneToMany,
MaxHTTPBufferSize:    DefaultMaxHTTPBufferSize,
MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
NetworkTTLS:          DefaultNetworkTTLS,
}
svc := NewCorrelationService(config, timeProvider)

// A1 arrives first — no B yet → pending orphan
a1 := &NormalizedEvent{
Source:    SourceA,
Timestamp: now,
SrcIP:     "10.0.0.1",
SrcPort:   5555,
}
results := svc.ProcessEvent(a1)
if len(results) != 0 {
t.Fatalf("A1: expected 0 results (pending orphan), got %d", len(results))
}

// B arrives within the delay window — should correlate with A1
b := &NormalizedEvent{
Source:    SourceB,
Timestamp: now.Add(200 * time.Millisecond),
SrcIP:     "10.0.0.1",
SrcPort:   5555,
}
timeProvider.now = now.Add(200 * time.Millisecond)
results = svc.ProcessEvent(b)
if len(results) != 1 || results[0].Correlated == 0 {
t.Fatalf("B: expected 1 correlated result (A1+B), got %d correlated=%v",
len(results), len(results) > 0 && results[0].Correlated != 0)
}

// A2 arrives on the same Keep-Alive connection — B must still be in buffer
a2 := &NormalizedEvent{
Source:    SourceA,
Timestamp: now.Add(400 * time.Millisecond),
SrcIP:     "10.0.0.1",
SrcPort:   5555,
}
timeProvider.now = now.Add(400 * time.Millisecond)
results = svc.ProcessEvent(a2)

// A2 should correlate with B (still in buffer in one_to_many mode)
correlated := false
for _, r := range results {
if r.Correlated != 0 {
correlated = true
}
}
if !correlated {
t.Errorf("A2 Keep-Alive: expected correlated result, got %d results — B was not buffered after pending orphan match (bug)", len(results))
}
}

// TestKeepAlive_BeyondTimeWindow_StillCorrelates verifies that in one_to_many mode,
// A events are correlated with the buffered B event even when the time difference
// between A.Timestamp and B.original Timestamp exceeds TimeWindow.
// Regression test for Bug #1.
func TestKeepAlive_BeyondTimeWindow_StillCorrelates(t *testing.T) {
now := time.Date(2026, 3, 5, 16, 30, 0, 0, time.UTC)
tp := &mockTimeProvider{now: now}

config := CorrelationConfig{
TimeWindow:           10 * time.Second,
ApacheAlwaysEmit:     true,
ApacheEmitDelayMs:    500,
NetworkTTLS:          120,
MatchingMode:         MatchingModeOneToMany,
MaxHTTPBufferSize:    DefaultMaxHTTPBufferSize,
MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
}
svc := NewCorrelationService(config, tp)

// B arrives first (TCP connection established)
b := &NormalizedEvent{
Source:    SourceB,
Timestamp: now,
SrcIP:     "91.224.92.185",
SrcPort:   53471,
}
if results := svc.ProcessEvent(b); len(results) != 0 {
t.Fatalf("B buffered: expected 0 results, got %d", len(results))
}

// A seq=1 arrives within window -- correlates and resets TTL
tp.now = now.Add(100 * time.Millisecond)
a1 := &NormalizedEvent{
Source:    SourceA,
Timestamp: tp.now,
SrcIP:     "91.224.92.185",
SrcPort:   53471,
}
if results := svc.ProcessEvent(a1); len(results) != 1 || results[0].Correlated == 0 {
t.Fatalf("A seq=1: expected 1 correlated result, got %d", len(svc.ProcessEvent(a1)))
}

// A seq=10 arrives 11s after B (beyond TimeWindow) but B TTL is still valid (reset at seq=1)
tp.now = now.Add(11 * time.Second)
a10 := &NormalizedEvent{
Source:    SourceA,
Timestamp: tp.now,
SrcIP:     "91.224.92.185",
SrcPort:   53471,
}
results := svc.ProcessEvent(a10)

correlated := false
for _, r := range results {
if r.Correlated != 0 {
correlated = true
}
}
if !correlated {
t.Errorf("A seq=10 (diff>TimeWindow, TTL valid): expected Correlated=true, got %d results -- Bug #1 not fixed", len(results))
}
}

// TestKeepAlive_PendingOrphan_MatchesBeyondTimeWindow verifies that a pending orphan A
// is correctly matched by an incoming B when timestamps differ by more than TimeWindow.
// Regression test for Bug #4.
func TestKeepAlive_PendingOrphan_MatchesBeyondTimeWindow(t *testing.T) {
now := time.Date(2026, 3, 5, 16, 30, 0, 0, time.UTC)
tp := &mockTimeProvider{now: now}

config := CorrelationConfig{
TimeWindow:           10 * time.Second,
ApacheAlwaysEmit:     true,
ApacheEmitDelayMs:    500,
NetworkTTLS:          120,
MatchingMode:         MatchingModeOneToMany,
MaxHTTPBufferSize:    DefaultMaxHTTPBufferSize,
MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
}
svc := NewCorrelationService(config, tp)

// A arrives first, no B in buffer -> pending orphan
a := &NormalizedEvent{
Source:    SourceA,
Timestamp: now.Add(15 * time.Second),
SrcIP:     "10.1.2.3",
SrcPort:   9999,
}
if results := svc.ProcessEvent(a); len(results) != 0 {
t.Fatalf("A with no B: expected 0 results (pending orphan), got %d", len(results))
}
if len(svc.pendingOrphans) != 1 {
t.Fatalf("expected 1 pending orphan key, got %d", len(svc.pendingOrphans))
}

// B arrives with old timestamp (diff with A = 15s > TimeWindow)
// In one_to_many: same key = same connection, must match pending orphan regardless of timestamps
b := &NormalizedEvent{
Source:    SourceB,
Timestamp: now,
SrcIP:     "10.1.2.3",
SrcPort:   9999,
}
results := svc.ProcessEvent(b)

correlated := false
for _, r := range results {
if r.Correlated != 0 {
correlated = true
}
}
if !correlated {
t.Errorf("pending orphan + B (diff>TimeWindow): expected Correlated=true -- Bug #4 not fixed")
}
if len(svc.pendingOrphans) != 0 {
t.Errorf("expected pendingOrphans empty after match, got %d keys", len(svc.pendingOrphans))
}
}

// TestBTTLExpiry_PurgesPendingOrphans verifies that when a B event TTL expires,
// pending orphan A events for that key are emitted immediately (not lost).
// Regression test for Bug #3 — including the data-loss fix.
func TestBTTLExpiry_PurgesPendingOrphans(t *testing.T) {
	now := time.Date(2026, 3, 5, 16, 30, 0, 0, time.UTC)
	tp := &mockTimeProvider{now: now}

	// Use one_to_one + tight TimeWindow so A goes to pending orphans
	config := CorrelationConfig{
		TimeWindow:           2 * time.Second,
		ApacheAlwaysEmit:     true,
		ApacheEmitDelayMs:    500,
		NetworkTTLS:          5,
		MatchingMode:         MatchingModeOneToOne,
		MaxHTTPBufferSize:    DefaultMaxHTTPBufferSize,
		MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
	}
	svc := NewCorrelationService(config, tp)

	// B arrives
	svc.ProcessEvent(&NormalizedEvent{
		Source: SourceB, Timestamp: now, SrcIP: "10.9.9.9", SrcPort: 8888,
	})

	// A arrives 3s later -- beyond TimeWindow(2s), no match -> pending orphan
	tp.now = now.Add(3 * time.Second)
	svc.ProcessEvent(&NormalizedEvent{
		Source: SourceA, Timestamp: tp.now, SrcIP: "10.9.9.9", SrcPort: 8888,
	})

	if _, exists := svc.pendingOrphans["10.9.9.9:8888"]; !exists {
		t.Skip("A not in pendingOrphans (different code path) -- skipping")
	}

	// Advance past B TTL (5s from t=0, now at t=6s)
	// Trigger cleanExpired via ProcessEvent with a dummy event; collect returned logs
	tp.now = now.Add(6 * time.Second)
	returned := svc.ProcessEvent(&NormalizedEvent{
		Source: SourceB, Timestamp: tp.now, SrcIP: "99.99.99.1", SrcPort: 1,
	})

	// pendingOrphans must be cleared
	if _, exists := svc.pendingOrphans["10.9.9.9:8888"]; exists {
		t.Errorf("Bug #3: pendingOrphans not purged after B TTL expired")
	}

	// The orphan must have been returned (not silently lost) — data-loss fix
	orphanFound := false
	for _, r := range returned {
		if r.Correlated == 0 && r.SrcIP == "10.9.9.9" {
			orphanFound = true
		}
	}
	if !orphanFound {
		t.Errorf("Bug #3 data-loss: orphan A not returned when B TTL expired (got %d results)", len(returned))
	}
}

// TestEmitPendingOrphans_PublicMethod verifies EmitPendingOrphans() emits orphans
// after the delay expires and nothing before. Regression test for Bug #2.
func TestEmitPendingOrphans_PublicMethod(t *testing.T) {
now := time.Date(2026, 3, 5, 16, 30, 0, 0, time.UTC)
tp := &mockTimeProvider{now: now}

config := CorrelationConfig{
TimeWindow:           10 * time.Second,
ApacheAlwaysEmit:     true,
ApacheEmitDelayMs:    500,
NetworkTTLS:          120,
MatchingMode:         MatchingModeOneToMany,
MaxHTTPBufferSize:    DefaultMaxHTTPBufferSize,
MaxNetworkBufferSize: DefaultMaxNetworkBufferSize,
}
svc := NewCorrelationService(config, tp)

// A arrives with no B -> pending orphan
if results := svc.ProcessEvent(&NormalizedEvent{
Source: SourceA, Timestamp: now, SrcIP: "192.0.2.1", SrcPort: 4444,
}); len(results) != 0 {
t.Fatalf("expected 0 results (pending), got %d", len(results))
}

// Before delay: nothing emitted
tp.now = now.Add(200 * time.Millisecond)
if emitted := svc.EmitPendingOrphans(); len(emitted) != 0 {
t.Errorf("before delay: expected 0 emitted orphans, got %d", len(emitted))
}

// After delay (>500ms): orphan emitted
tp.now = now.Add(600 * time.Millisecond)
emitted := svc.EmitPendingOrphans()
if len(emitted) != 1 {
t.Fatalf("after delay: expected 1 emitted orphan, got %d", len(emitted))
}
if emitted[0].Correlated != 0 {
t.Errorf("expected orphan (Correlated=false), got Correlated=true")
}

// Second call: nothing (already emitted)
if emitted2 := svc.EmitPendingOrphans(); len(emitted2) != 0 {
t.Errorf("second call: expected 0 (already emitted), got %d", len(emitted2))
}
}
