package errorbus

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

	"git.dcentral.systems/toolz/goplt/pkg/logger"
)

func TestNewChannelBus(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 100)

	if bus == nil {
		t.Fatal("Expected bus, got nil")
	}

	if bus.errors == nil {
		t.Error("Expected errors channel, got nil")
	}

	if bus.logger == nil {
		t.Error("Expected logger, got nil")
	}

	// Clean up
	_ = bus.Close()
}

func TestNewChannelBus_DefaultBufferSize(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 0)

	if bus == nil {
		t.Fatal("Expected bus, got nil")
	}

	// Clean up
	_ = bus.Close()
}

func TestChannelBus_Publish(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 10)

	testErr := errors.New("test error")
	ctx := context.Background()

	// Publish error
	bus.Publish(ctx, testErr)

	// Wait a bit for the error to be processed
	time.Sleep(100 * time.Millisecond)

	// Verify error was logged
	mockLogger.mu.Lock()
	errorCount := len(mockLogger.errors)
	mockLogger.mu.Unlock()
	if errorCount == 0 {
		t.Error("Expected error to be logged")
	}

	// Clean up
	_ = bus.Close()
}

func TestChannelBus_Publish_NilError(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 10)

	ctx := context.Background()

	// Publish nil error (should be ignored)
	bus.Publish(ctx, nil)

	// Wait a bit
	time.Sleep(50 * time.Millisecond)

	// Verify nil error was not logged
	mockLogger.mu.Lock()
	errorCount := len(mockLogger.errors)
	mockLogger.mu.Unlock()
	if errorCount > 0 {
		t.Error("Expected nil error to be ignored")
	}

	// Clean up
	_ = bus.Close()
}

func TestChannelBus_Publish_WithContext(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 10)

	testErr := errors.New("test error")
	type contextKey string
	const requestIDKey contextKey = "request_id"
	ctx := context.WithValue(context.Background(), requestIDKey, "test-request-id")

	bus.Publish(ctx, testErr)

	// Wait for processing
	time.Sleep(100 * time.Millisecond)

	// Verify error was logged with context
	mockLogger.mu.Lock()
	errorCount := len(mockLogger.errors)
	mockLogger.mu.Unlock()
	if errorCount == 0 {
		t.Error("Expected error to be logged")
	}

	// Clean up
	_ = bus.Close()
}

func TestChannelBus_Close(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 10)

	// Publish some errors
	for i := 0; i < 5; i++ {
		bus.Publish(context.Background(), errors.New("test error"))
	}

	// Close and wait
	if err := bus.Close(); err != nil {
		t.Errorf("Close failed: %v", err)
	}

	// Verify channel is closed
	select {
	case <-bus.errors:
		// Channel is closed, this is expected
	default:
		t.Error("Expected errors channel to be closed")
	}
}

func TestChannelBus_Close_MultipleTimes(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	bus := NewChannelBus(mockLogger, 10)

	// Close first time
	if err := bus.Close(); err != nil {
		t.Errorf("First Close failed: %v", err)
	}

	// Close second time should be safe (uses sync.Once)
	// The channel is already closed, but Close() should handle this gracefully
	if err := bus.Close(); err != nil {
		t.Errorf("Second Close failed: %v", err)
	}
}

func TestChannelBus_ChannelFull(t *testing.T) {
	t.Parallel()

	mockLogger := &mockLogger{}
	// Use small buffer to test channel full scenario
	bus := NewChannelBus(mockLogger, 1)

	// Fill the channel
	bus.Publish(context.Background(), errors.New("error1"))

	// This should not block (channel is full, should log directly)
	bus.Publish(context.Background(), errors.New("error2"))

	// Wait a bit
	time.Sleep(100 * time.Millisecond)

	// Clean up
	_ = bus.Close()
}

// mockLogger implements logger.Logger for testing.
type mockLogger struct {
	errors []string
	mu     sync.Mutex
}

func (m *mockLogger) Debug(msg string, fields ...logger.Field) {}
func (m *mockLogger) Info(msg string, fields ...logger.Field)  {}
func (m *mockLogger) Warn(msg string, fields ...logger.Field)  {}
func (m *mockLogger) Error(msg string, fields ...logger.Field) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.errors = append(m.errors, msg)
}

func (m *mockLogger) With(fields ...logger.Field) logger.Logger {
	return m
}

func (m *mockLogger) WithContext(ctx context.Context) logger.Logger {
	return m
}
