package clickhouse

import (
	"context"
	"testing"
	"time"

	"github.com/antitbone/ja4/correlator/internal/domain"
	"github.com/antitbone/ja4/correlator/internal/observability"
)

func TestClickHouseSink_Name(t *testing.T) {
	sink := &ClickHouseSink{
		config: Config{
			DSN:   "clickhouse://test:test@localhost:9000/test",
			Table: "test_table",
		},
	}

	if sink.Name() != "clickhouse" {
		t.Errorf("expected name 'clickhouse', got %s", sink.Name())
	}
}

func TestClickHouseSink_ConfigDefaults(t *testing.T) {
	// Test that defaults are applied correctly
	config := Config{
		DSN:   "clickhouse://test:test@localhost:9000/test",
		Table: "test_table",
		// Other fields are zero, should get defaults
	}

	// Verify defaults would be applied (we can't actually connect in tests)
	if config.BatchSize <= 0 {
		config.BatchSize = DefaultBatchSize
	}
	if config.FlushIntervalMs <= 0 {
		config.FlushIntervalMs = DefaultFlushIntervalMs
	}
	if config.MaxBufferSize <= 0 {
		config.MaxBufferSize = DefaultMaxBufferSize
	}
	if config.TimeoutMs <= 0 {
		config.TimeoutMs = DefaultTimeoutMs
	}

	if config.BatchSize != DefaultBatchSize {
		t.Errorf("expected BatchSize %d, got %d", DefaultBatchSize, config.BatchSize)
	}
	if config.FlushIntervalMs != DefaultFlushIntervalMs {
		t.Errorf("expected FlushIntervalMs %d, got %d", DefaultFlushIntervalMs, config.FlushIntervalMs)
	}
	if config.MaxBufferSize != DefaultMaxBufferSize {
		t.Errorf("expected MaxBufferSize %d, got %d", DefaultMaxBufferSize, config.MaxBufferSize)
	}
	if config.TimeoutMs != DefaultTimeoutMs {
		t.Errorf("expected TimeoutMs %d, got %d", DefaultTimeoutMs, config.TimeoutMs)
	}
}

func TestClickHouseSink_Write_BufferOverflow(t *testing.T) {
	// This test verifies the buffer overflow logic without actually connecting
	config := Config{
		DSN:             "clickhouse://test:test@localhost:9000/test",
		Table:           "test_table",
		BatchSize:       10,
		MaxBufferSize:   10,
		DropOnOverflow:  true,
		TimeoutMs:       100,
		FlushIntervalMs: 1000,
	}

	// We can't test actual writes without a ClickHouse instance,
	// but we can verify the config is valid
	if config.BatchSize > config.MaxBufferSize {
		t.Error("BatchSize should not exceed MaxBufferSize")
	}
}

func TestClickHouseSink_IsRetryableError(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected bool
	}{
		{"nil error", nil, false},
		{"connection refused", &mockError{"connection refused"}, true},
		{"connection reset", &mockError{"connection reset by peer"}, true},
		{"timeout", &mockError{"timeout waiting for response"}, true},
		{"network unreachable", &mockError{"network is unreachable"}, true},
		{"broken pipe", &mockError{"broken pipe"}, true},
		{"syntax error", &mockError{"syntax error in SQL"}, false},
		{"table not found", &mockError{"table test not found"}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := isRetryableError(tt.err)
			if result != tt.expected {
				t.Errorf("expected %v, got %v", tt.expected, result)
			}
		})
	}
}

func TestClickHouseSink_FlushEmpty(t *testing.T) {
	// Test that flushing an empty buffer doesn't cause issues
	// (We can't test actual ClickHouse operations without a real instance)

	s := &ClickHouseSink{
		config: Config{
			DSN:   "clickhouse://test:test@localhost:9000/test",
			Table: "test_table",
		},
		buffer: make([]domain.CorrelatedLog, 0),
	}

	// Should not panic or error on empty flush
	ctx := context.Background()
	err := s.Flush(ctx)
	if err != nil {
		t.Errorf("expected no error on empty flush, got %v", err)
	}
}

func TestClickHouseSink_CloseWithoutConnect(t *testing.T) {
	// Test that closing without connecting doesn't panic
	s := &ClickHouseSink{
		config: Config{
			DSN:   "clickhouse://test:test@localhost:9000/test",
			Table: "test_table",
		},
		buffer: make([]domain.CorrelatedLog, 0),
		done:   make(chan struct{}),
	}

	err := s.Close()
	if err != nil {
		t.Errorf("expected no error on close without connect, got %v", err)
	}
}

func TestClickHouseSink_Constants(t *testing.T) {
	// Verify constants have reasonable values
	if DefaultBatchSize <= 0 {
		t.Error("DefaultBatchSize should be positive")
	}
	if DefaultFlushIntervalMs <= 0 {
		t.Error("DefaultFlushIntervalMs should be positive")
	}
	if DefaultMaxBufferSize <= 0 {
		t.Error("DefaultMaxBufferSize should be positive")
	}
	if DefaultTimeoutMs <= 0 {
		t.Error("DefaultTimeoutMs should be positive")
	}
	if DefaultPingTimeoutMs <= 0 {
		t.Error("DefaultPingTimeoutMs should be positive")
	}
	if MaxRetries <= 0 {
		t.Error("MaxRetries should be positive")
	}
	if RetryBaseDelay <= 0 {
		t.Error("RetryBaseDelay should be positive")
	}
}

// mockError implements error for testing
type mockError struct {
	msg string
}

func (e *mockError) Error() string {
	return e.msg
}

// Test the doFlush function with empty buffer (no actual DB connection)
func TestClickHouseSink_DoFlushEmpty(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{
			DSN:   "clickhouse://test:test@localhost:9000/test",
			Table: "test_table",
		},
		buffer: make([]domain.CorrelatedLog, 0),
	}

	ctx := context.Background()
	err := s.doFlush(ctx)
	if err != nil {
		t.Errorf("expected no error when flushing empty buffer, got %v", err)
	}
}

// Test that buffer is properly managed (without actual DB operations)
func TestClickHouseSink_BufferManagement(t *testing.T) {
	log := domain.CorrelatedLog{
		SrcIP:      "192.168.1.1",
		SrcPort:    8080,
		Correlated: 1,
	}

	s := &ClickHouseSink{
		config: Config{
			DSN:             "clickhouse://test:test@localhost:9000/test",
			Table:           "test_table",
			MaxBufferSize:   100, // Allow more than 1 element
			DropOnOverflow:  false,
			TimeoutMs:       1000,
		},
		buffer: []domain.CorrelatedLog{log},
	}

	// Verify buffer has data
	if len(s.buffer) != 1 {
		t.Fatalf("expected buffer length 1, got %d", len(s.buffer))
	}

	// Test that Write properly adds to buffer
	ctx := context.Background()
	err := s.Write(ctx, log)
	if err != nil {
		t.Errorf("unexpected error on Write: %v", err)
	}

	if len(s.buffer) != 2 {
		t.Errorf("expected buffer length 2 after Write, got %d", len(s.buffer))
	}
}

// Test Write with context cancellation
func TestClickHouseSink_Write_ContextCancel(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{
			DSN:             "clickhouse://test:test@localhost:9000/test",
			Table:           "test_table",
			MaxBufferSize:   1,
			DropOnOverflow:  false,
			TimeoutMs:       10,
		},
		buffer: make([]domain.CorrelatedLog, 0, 1),
	}

	// Fill the buffer
	log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
	s.buffer = append(s.buffer, log)

	// Try to write with cancelled context
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	err := s.Write(ctx, log)
	if err == nil {
		t.Error("expected error when writing with cancelled context")
	}
}

// Test DropOnOverflow behavior
func TestClickHouseSink_Write_DropOnOverflow(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{
			DSN:             "clickhouse://test:test@localhost:9000/test",
			Table:           "test_table",
			MaxBufferSize:   1,
			DropOnOverflow:  true,
			TimeoutMs:       10,
		},
		buffer: make([]domain.CorrelatedLog, 0, 1),
	}

	// Fill the buffer
	log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
	s.buffer = append(s.buffer, log)

	// Try to write when buffer is full - should drop silently
	ctx := context.Background()
	err := s.Write(ctx, log)
	if err != nil {
		t.Errorf("expected no error when DropOnOverflow is true, got %v", err)
	}
}

// TestIsRetryableError_ContextDeadlineExceeded tests context.DeadlineExceeded is retryable.
func TestIsRetryableError_ContextDeadlineExceeded(t *testing.T) {
	if !isRetryableError(context.DeadlineExceeded) {
		t.Error("context.DeadlineExceeded should be retryable")
	}
}

// TestIsRetryableError_ContextCanceled tests context.Canceled is NOT retryable.
func TestIsRetryableError_ContextCanceled(t *testing.T) {
	if isRetryableError(context.Canceled) {
		t.Error("context.Canceled should not be retryable")
	}
}

// TestIsRetryableError_NetTimeout tests net.Error with Timeout() = true is retryable.
func TestIsRetryableError_NetTimeout(t *testing.T) {
	err := &mockNetError{timeout: true, temporary: false}
	if !isRetryableError(err) {
		t.Error("net.Error with Timeout()=true should be retryable")
	}
}

// TestIsRetryableError_NetNoTimeout tests net.Error with Timeout() = false is NOT retryable.
func TestIsRetryableError_NetNoTimeout(t *testing.T) {
	err := &mockNetError{timeout: false, temporary: false}
	if isRetryableError(err) {
		t.Error("net.Error with Timeout()=false should not be retryable (unless msg matches)")
	}
}

// TestIsRetryableError_UnknownTable tests "unknown table" is NOT retryable.
func TestIsRetryableError_UnknownTable(t *testing.T) {
	if isRetryableError(&mockError{"unknown table users"}) {
		t.Error("unknown table error should not be retryable")
	}
}

// TestIsRetryableError_UnknownColumn tests "unknown column" is NOT retryable.
func TestIsRetryableError_UnknownColumn(t *testing.T) {
	if isRetryableError(&mockError{"unknown column foo"}) {
		t.Error("unknown column error should not be retryable")
	}
}

// TestIsRetryableError_RandomError tests a random error is NOT retryable.
func TestIsRetryableError_RandomError(t *testing.T) {
	if isRetryableError(&mockError{"some random unrecognized error"}) {
		t.Error("random error should not be retryable")
	}
}

// TestIsRetryableError_NoRouteToHost tests "no route to host" is retryable.
func TestIsRetryableError_NoRouteToHost(t *testing.T) {
	if !isRetryableError(&mockError{"no route to host"}) {
		t.Error("'no route to host' should be retryable")
	}
}

// TestIsRetryableError_TemporaryFailure tests "temporary failure" is retryable.
func TestIsRetryableError_TemporaryFailure(t *testing.T) {
	if !isRetryableError(&mockError{"temporary failure in name resolution"}) {
		t.Error("'temporary failure' should be retryable")
	}
}

// mockNetError implements net.Error for testing.
type mockNetError struct {
	timeout   bool
	temporary bool
	msg       string
}

func (e *mockNetError) Error() string   { return e.msg }
func (e *mockNetError) Timeout() bool   { return e.timeout }
func (e *mockNetError) Temporary() bool { return e.temporary }

// TestNewClickHouseSink_EmptyDSN tests that empty DSN returns error.
func TestNewClickHouseSink_EmptyDSN(t *testing.T) {
	_, err := NewClickHouseSink(Config{
		DSN:   "",
		Table: "test_table",
	})
	if err == nil {
		t.Error("expected error for empty DSN")
	}
}

// TestNewClickHouseSink_WhitespaceDSN tests that whitespace DSN returns error.
func TestNewClickHouseSink_WhitespaceDSN(t *testing.T) {
	_, err := NewClickHouseSink(Config{
		DSN:   "   ",
		Table: "test_table",
	})
	if err == nil {
		t.Error("expected error for whitespace-only DSN")
	}
}

// TestNewClickHouseSink_EmptyTable tests that empty Table returns error.
func TestNewClickHouseSink_EmptyTable(t *testing.T) {
	_, err := NewClickHouseSink(Config{
		DSN:   "clickhouse://localhost:9000/test",
		Table: "",
	})
	if err == nil {
		t.Error("expected error for empty Table")
	}
}

// TestNewClickHouseSink_WhitespaceTable tests that whitespace Table returns error.
func TestNewClickHouseSink_WhitespaceTable(t *testing.T) {
	_, err := NewClickHouseSink(Config{
		DSN:   "clickhouse://localhost:9000/test",
		Table: "   ",
	})
	if err == nil {
		t.Error("expected error for whitespace-only Table")
	}
}

// TestNewClickHouseSink_InvalidDSN tests that an invalid DSN (no real connection) returns error.
func TestNewClickHouseSink_InvalidDSN(t *testing.T) {
	_, err := NewClickHouseSink(Config{
		DSN:   "not-a-valid-dsn",
		Table: "test_table",
	})
	if err == nil {
		t.Error("expected error for invalid DSN")
	}
}

// TestClickHouseSink_SetLogger tests that SetLogger sets a logger.
func TestClickHouseSink_SetLogger(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{Table: "test_table"},
		buffer: make([]domain.CorrelatedLog, 0),
	}

	testLogger := observability.NewLogger("test")
	s.SetLogger(testLogger)

	if s.logger == nil {
		t.Error("expected logger to be set")
	}
}

// TestClickHouseSink_LogNilLogger tests that log() returns a logger even when s.logger is nil.
func TestClickHouseSink_LogNilLogger(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{Table: "test_table"},
		buffer: make([]domain.CorrelatedLog, 0),
	}
	s.logger = nil

	// log() should auto-initialize
	logger := s.log()
	if logger == nil {
		t.Error("expected non-nil logger from log()")
	}
}

// TestClickHouseSink_Reopen tests that Reopen is a no-op and returns nil.
func TestClickHouseSink_Reopen(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{Table: "test_table"},
		buffer: make([]domain.CorrelatedLog, 0),
	}
	if err := s.Reopen(); err != nil {
		t.Errorf("Reopen() should return nil, got: %v", err)
	}
}

// TestClickHouseSink_DoFlushNilConn tests doFlush returns error when conn is nil and buffer non-empty.
func TestClickHouseSink_DoFlushNilConn(t *testing.T) {
	log := domain.CorrelatedLog{SrcIP: "1.2.3.4", SrcPort: 1234}
	s := &ClickHouseSink{
		config: Config{
			Table:     "test_table",
			BatchSize: DefaultBatchSize,
		},
		buffer: []domain.CorrelatedLog{log},
		conn:   nil,
	}

	err := s.doFlush(context.Background())
	if err == nil {
		t.Error("expected error from doFlush when conn is nil")
	}
}

// TestClickHouseSink_CloseTwice tests that calling Close() twice does not panic or error.
func TestClickHouseSink_CloseTwice(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{
			Table:     "test_table",
			TimeoutMs: DefaultTimeoutMs,
		},
		buffer: make([]domain.CorrelatedLog, 0),
		done:   make(chan struct{}),
	}

	if err := s.Close(); err != nil {
		t.Errorf("first Close() should not error, got: %v", err)
	}
	if err := s.Close(); err != nil {
		t.Errorf("second Close() should not error (closeOnce), got: %v", err)
	}
}

// TestClickHouseSink_WriteTimeout tests that Write returns error when buffer is full and timeout exceeded.
func TestClickHouseSink_Write_Timeout(t *testing.T) {
	s := &ClickHouseSink{
		config: Config{
			Table:          "test_table",
			MaxBufferSize:  1,
			DropOnOverflow: false,
			TimeoutMs:      1, // 1ms timeout
		},
		buffer: make([]domain.CorrelatedLog, 0, 1),
	}

	log := domain.CorrelatedLog{SrcIP: "1.2.3.4", SrcPort: 1234}
	// Fill the buffer
	s.buffer = append(s.buffer, log)

	ctx := context.Background()
	err := s.Write(ctx, log)
	if err == nil {
		t.Error("expected error when buffer full and timeout exceeded")
	}
}

// Benchmark Write operation (without actual DB)
func BenchmarkClickHouseSink_Write(b *testing.B) {
	s := &ClickHouseSink{
		config: Config{
			DSN:             "clickhouse://test:test@localhost:9000/test",
			Table:           "test_table",
			MaxBufferSize:   10000,
			DropOnOverflow:  true,
		},
		buffer: make([]domain.CorrelatedLog, 0, 10000),
	}

	log := domain.CorrelatedLog{
		Timestamp:  time.Now(),
		SrcIP:      "192.168.1.1",
		SrcPort:    8080,
		Correlated: 1,
	}

	ctx := context.Background()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		s.Write(ctx, log)
	}
}
