package events

import (
	"context"
	"errors"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestEventBus_SubscribeAndPublish(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("TEST_EVENT")

	var receivedPayload interface{}
	var wg sync.WaitGroup
	wg.Add(1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		receivedPayload = payload
		wg.Done()
		return nil
	})

	err := eb.Publish(context.Background(), eventType, "test_data")
	require.NoError(t, err)

	wg.Wait()
	assert.Equal(t, "test_data", receivedPayload)
}

func TestEventBus_PublishError(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("ERROR_EVENT")

	expectedErr := errors.New("handler failed")
	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		return expectedErr
	})

	err := eb.Publish(context.Background(), eventType, "data")
	assert.Error(t, err)
	assert.ErrorIs(t, err, expectedErr)
}

func TestEventBus_PublishPreservesAppError(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("VALIDATION_EVENT")

	expectedErr := apperrors.NewValidationError("", "Quote must be Accepted before creating an Order.")
	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		return expectedErr
	})

	err := eb.Publish(context.Background(), eventType, "data")
	var validationErr *apperrors.ValidationError
	assert.ErrorAs(t, err, &validationErr)
	assert.Equal(t, expectedErr.Error(), err.Error())
}

func TestEventBus_PanicRecovery_Sync(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("PANIC_EVENT")

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		panic("boom")
	})

	// Should not panic the test
	// Publish catches panic and returns it as error
	assert.NotPanics(t, func() {
		err := eb.Publish(context.Background(), eventType, "data")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "panic in event handler")
	})
}

func TestEventBus_PublishAsync(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("ASYNC_EVENT")

	resultChan := make(chan string, 1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		time.Sleep(10 * time.Millisecond) // Simulate work
		resultChan <- payload.(string)
		return nil
	})

	eb.PublishAsync(eventType, "async_data")

	select {
	case res := <-resultChan:
		assert.Equal(t, "async_data", res)
	case <-time.After(1 * time.Second):
		t.Fatal("Async handler did not complete in time")
	}
}

func TestEventBus_MultipleHandlers(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("MULTI_EVENT")

	var count int
	var mu sync.Mutex

	handler := func(ctx context.Context, payload interface{}) error {
		mu.Lock()
		count++
		mu.Unlock()
		return nil
	}

	eb.Subscribe(eventType, handler)
	eb.Subscribe(eventType, handler)

	err := eb.Publish(context.Background(), eventType, "data")
	require.NoError(t, err)

	assert.Equal(t, 2, count)
}

func TestEventBus_Unsubscribe(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("UNSUB_EVENT")

	var count int
	handler := func(ctx context.Context, payload interface{}) error {
		count++
		return nil
	}

	unsub := eb.Subscribe(eventType, handler)

	// First publish
	_ = eb.Publish(context.Background(), eventType, "1")
	assert.Equal(t, 1, count)

	// Unsubscribe
	unsub()

	// Second publish
	_ = eb.Publish(context.Background(), eventType, "2")
	assert.Equal(t, 1, count, "Handler should not be called after unsubscribe")
}

func TestNewEventBusWithQueueSize(t *testing.T) {
	tests := []struct {
		name         string
		queueSize    int
		expectedSize int
	}{
		{"positive queue size", 50, 50},
		{"zero defaults to DefaultQueueSize", 0, DefaultQueueSize},
		{"negative defaults to DefaultQueueSize", -1, DefaultQueueSize},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			eb := NewEventBusWithQueueSize(tc.queueSize)
			require.NotNil(t, eb)
			assert.Equal(t, tc.expectedSize, eb.queueSize)
			assert.Equal(t, tc.expectedSize, cap(eb.semaphore))
			assert.NotNil(t, eb.handlers)
		})
	}
}

func TestEventBus_SetMetricsCallbacks(t *testing.T) {
	eb := NewEventBus()

	var droppedType string
	var utilization float64
	callbacks := MetricsCallbacks{
		OnEventDropped: func(eventType string) {
			droppedType = eventType
		},
		OnQueueUtilizationChange: func(current float64) {
			utilization = current
		},
	}

	eb.SetMetricsCallbacks(callbacks)

	// Verify callbacks are stored by triggering them
	eb.recordEventDropped("test.event")
	assert.Equal(t, "test.event", droppedType)

	eb.recordQueueUtilization()
	assert.Equal(t, float64(0), utilization)
}

func TestEventBus_Clear(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("CLEAR_EVENT")

	var count int
	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		count++
		return nil
	})

	// Publish before clear
	_ = eb.Publish(context.Background(), eventType, "data")
	assert.Equal(t, 1, count)

	// Clear all handlers
	eb.Clear()

	// Publish after clear - handler should not be called
	_ = eb.Publish(context.Background(), eventType, "data")
	assert.Equal(t, 1, count, "Handler should not be called after Clear")
}

func TestEventBus_PublishToNoHandlers(t *testing.T) {
	eb := NewEventBus()
	// Publishing to an event type with no subscribers should return nil
	err := eb.Publish(context.Background(), EventType("NO_HANDLERS"), "data")
	assert.NoError(t, err)
}

func TestEventBus_PublishAsync_QueueFull(t *testing.T) {
	// Create bus with queue size of 1
	eb := NewEventBusWithQueueSize(1)
	eventType := EventType("QUEUE_FULL_EVENT")

	var droppedCount int32
	eb.SetMetricsCallbacks(MetricsCallbacks{
		OnEventDropped: func(et string) {
			atomic.AddInt32(&droppedCount, 1)
		},
	})

	blockCh := make(chan struct{})

	// Subscribe a handler that blocks
	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		<-blockCh
		return nil
	})

	// First async publish should acquire the semaphore
	eb.PublishAsync(eventType, "first")

	// Give the goroutine time to start and acquire semaphore
	time.Sleep(50 * time.Millisecond)

	// Second async publish should be dropped (queue full)
	eb.PublishAsync(eventType, "second")

	// Allow a moment for the drop callback to fire
	time.Sleep(10 * time.Millisecond)
	assert.Equal(t, int32(1), atomic.LoadInt32(&droppedCount))

	// Unblock the first handler
	close(blockCh)
}

func TestEventBus_PublishAsync_WithTraceContext(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("TRACE_ASYNC_EVENT")

	traceCh := make(chan string, 1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		tp, _ := ctx.Value(constants.ContextKeyTraceparent).(string)
		traceCh <- tp
		return nil
	})

	// PublishAsync with RecordEventPayload that includes traceparent in Metadata
	payload := RecordEventPayload{
		ObjectAPIName: "Account",
		Metadata: map[string]string{
			string(constants.ContextKeyTraceparent): "00-abc123-def456-01",
		},
	}
	eb.PublishAsync(eventType, payload)

	select {
	case tp := <-traceCh:
		assert.Equal(t, "00-abc123-def456-01", tp)
	case <-time.After(1 * time.Second):
		t.Fatal("Async handler did not complete in time")
	}
}

func TestEventBus_PublishAsyncWithContext(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("ASYNC_CTX_EVENT")

	traceCh := make(chan string, 1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		tp, _ := ctx.Value(constants.ContextKeyTraceparent).(string)
		traceCh <- tp
		return nil
	})

	// Create parent context with traceparent
	parentCtx := context.WithValue(context.Background(), constants.ContextKeyTraceparent, "00-trace-span-01")

	eb.PublishAsyncWithContext(parentCtx, eventType, "data")

	select {
	case tp := <-traceCh:
		assert.Equal(t, "00-trace-span-01", tp)
	case <-time.After(1 * time.Second):
		t.Fatal("Async handler did not complete in time")
	}
}

func TestEventBus_PublishAsyncWithContext_NoTrace(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("ASYNC_CTX_NO_TRACE")

	doneCh := make(chan string, 1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		tp, _ := ctx.Value(constants.ContextKeyTraceparent).(string)
		doneCh <- tp
		return nil
	})

	// Context without traceparent
	eb.PublishAsyncWithContext(context.Background(), eventType, "data")

	select {
	case tp := <-doneCh:
		assert.Empty(t, tp, "traceparent should be empty when not in context")
	case <-time.After(1 * time.Second):
		t.Fatal("Async handler did not complete in time")
	}
}

func TestEventBus_PublishAsyncWithContext_QueueFull(t *testing.T) {
	eb := NewEventBusWithQueueSize(1)
	eventType := EventType("ASYNC_CTX_FULL")

	var droppedCount int32
	eb.SetMetricsCallbacks(MetricsCallbacks{
		OnEventDropped: func(et string) {
			atomic.AddInt32(&droppedCount, 1)
		},
	})

	blockCh := make(chan struct{})

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		<-blockCh
		return nil
	})

	// Fill the queue
	eb.PublishAsyncWithContext(context.Background(), eventType, "first")
	time.Sleep(50 * time.Millisecond)

	// This should be dropped
	eb.PublishAsyncWithContext(context.Background(), eventType, "second")
	time.Sleep(10 * time.Millisecond)

	assert.Equal(t, int32(1), atomic.LoadInt32(&droppedCount))
	close(blockCh)
}

func TestEventBus_PublishAsync_PanicRecovery(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("ASYNC_PANIC")

	doneCh := make(chan struct{}, 1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		defer func() { doneCh <- struct{}{} }()
		panic("async boom")
	})

	assert.NotPanics(t, func() {
		eb.PublishAsync(eventType, "data")
	})

	select {
	case <-doneCh:
		// Panic was recovered
	case <-time.After(1 * time.Second):
		t.Fatal("Async panic handler did not complete in time")
	}
}

func TestEventBus_RecordQueueUtilization_NilCallback(t *testing.T) {
	eb := NewEventBus()
	// Should not panic when callbacks are not set
	assert.NotPanics(t, func() {
		eb.recordQueueUtilization()
	})
}

func TestEventBus_RecordEventDropped_NilCallback(t *testing.T) {
	eb := NewEventBus()
	// Should not panic when callbacks are not set
	assert.NotPanics(t, func() {
		eb.recordEventDropped("test.event")
	})
}

func TestEventBus_MetricsCallbacks_QueueUtilization(t *testing.T) {
	eb := NewEventBusWithQueueSize(10)
	eventType := EventType("METRICS_EVENT")

	var utilizations []float64
	var mu sync.Mutex

	eb.SetMetricsCallbacks(MetricsCallbacks{
		OnQueueUtilizationChange: func(current float64) {
			mu.Lock()
			utilizations = append(utilizations, current)
			mu.Unlock()
		},
	})

	doneCh := make(chan struct{})

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		close(doneCh)
		return nil
	})

	eb.PublishAsync(eventType, "data")

	select {
	case <-doneCh:
		// Handler completed
	case <-time.After(1 * time.Second):
		t.Fatal("Handler did not complete in time")
	}

	// Give time for semaphore release and metrics callback
	time.Sleep(50 * time.Millisecond)

	mu.Lock()
	defer mu.Unlock()
	// Should have recorded at least two utilization changes (acquire + release)
	assert.GreaterOrEqual(t, len(utilizations), 2)
}

func TestEventBus_Unsubscribe_Idempotent(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("UNSUB_IDEM")

	unsub := eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		return nil
	})

	// Unsubscribe twice should not panic
	unsub()
	assert.NotPanics(t, func() {
		unsub()
	})
}

func TestEventBus_Unsubscribe_CorrectHandler(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("UNSUB_CORRECT")

	var results []string

	unsub1 := eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		results = append(results, "handler1")
		return nil
	})

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		results = append(results, "handler2")
		return nil
	})

	// Unsubscribe first handler
	unsub1()

	_ = eb.Publish(context.Background(), eventType, "data")
	assert.Equal(t, []string{"handler2"}, results, "Only handler2 should remain after unsubscribing handler1")
}

func TestEventBus_PublishAsync_HandlerError(t *testing.T) {
	eb := NewEventBus()
	eventType := EventType("ASYNC_ERR")

	doneCh := make(chan struct{}, 1)

	eb.Subscribe(eventType, func(ctx context.Context, payload interface{}) error {
		defer func() { doneCh <- struct{}{} }()
		return errors.New("async handler error")
	})

	// Should not panic; error is logged
	assert.NotPanics(t, func() {
		eb.PublishAsync(eventType, "data")
	})

	select {
	case <-doneCh:
		// Handler completed (error was logged)
	case <-time.After(1 * time.Second):
		t.Fatal("Async handler did not complete in time")
	}
}
