package mca

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

	"github.com/baptisthealth/mca-sdk-go/config"
)

// TestNewClient_Success verifies that a valid configuration creates a client.
func TestNewClient_Success(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true, // Local dev
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() unexpected error: %v", err)
	}
	if client == nil {
		t.Fatal("NewClient() returned nil client")
	}

	// Verify client was initialized
	if client.config.ServiceName != "test-service" {
		t.Errorf("ServiceName = %q, want %q", client.config.ServiceName, "test-service")
	}
	if client.config.ModelID != "mdl-001" {
		t.Errorf("ModelID = %q, want %q", client.config.ModelID, "mdl-001")
	}

	// Cleanup
	if err := client.Shutdown(); err != nil {
		t.Errorf("Shutdown() error: %v", err)
	}
}

// TestNewClient_ValidationError verifies that invalid config returns error.
func TestNewClient_ValidationError(t *testing.T) {
	tests := []struct {
		name   string
		config *config.Config
	}{
		{
			name: "missing service name",
			config: &config.Config{
				ModelID:  "mdl-001",
				TeamName: "test-team",
			},
		},
		{
			name: "missing model id",
			config: &config.Config{
				ServiceName: "test-service",
				TeamName:    "test-team",
			},
		},
		{
			name: "missing team name",
			config: &config.Config{
				ServiceName: "test-service",
				ModelID:     "mdl-001",
			},
		},
	}

	ctx := context.Background()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client, err := NewClient(ctx, tt.config)
			if err == nil {
				t.Error("NewClient() expected error, got nil")
				if client != nil {
					_ = client.Shutdown()
				}
			}
			if !config.IsConfigurationError(err) {
				t.Errorf("NewClient() error type = %T, want ConfigurationError", err)
			}
		})
	}
}

// TestRecordPrediction_Success verifies prediction recording works.
func TestRecordPrediction_Success(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	latency := 0.15
	pred := &Prediction{
		Latency:      &latency,
		PredictionID: "pred-123",
		Attributes: map[string]string{
			"model_version": "2.0",
			"input_size":    "1024",
		},
	}

	err = client.RecordPrediction(context.Background(), pred)
	if err != nil {
		t.Errorf("RecordPrediction() unexpected error: %v", err)
	}
}

// TestRecordPrediction_WithZeroLatency verifies 0.0 latency is recorded.
func TestRecordPrediction_WithZeroLatency(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	latency := 0.0 // Valid zero latency
	pred := &Prediction{
		Latency:      &latency,
		PredictionID: "pred-zero",
	}

	err = client.RecordPrediction(context.Background(), pred)
	if err != nil {
		t.Errorf("RecordPrediction() unexpected error for 0.0 latency: %v", err)
	}
}

// TestRecordPrediction_NoLatency verifies recording without latency.
func TestRecordPrediction_NoLatency(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	pred := &Prediction{
		PredictionID: "pred-789",
		Latency:      nil, // No latency
	}

	err = client.RecordPrediction(context.Background(), pred)
	if err != nil {
		t.Errorf("RecordPrediction() unexpected error: %v", err)
	}
}

// TestRecordPrediction_AfterShutdown verifies error after shutdown.
func TestRecordPrediction_AfterShutdown(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}

	// Shutdown client
	if err := client.Shutdown(); err != nil {
		t.Errorf("Shutdown() error: %v", err)
	}

	// Try to record prediction after shutdown
	pred := &Prediction{PredictionID: "pred-after-shutdown"}
	err = client.RecordPrediction(context.Background(), pred)
	if err != ErrClientClosed {
		t.Errorf("RecordPrediction() after shutdown: got %v, want %v", err, ErrClientClosed)
	}
}

// TestRecordPrediction_AttributeLimit verifies attribute cardinality limits.
func TestRecordPrediction_AttributeLimit(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	// Create prediction with > 50 attributes
	attrs := make(map[string]string)
	for i := 0; i < 60; i++ {
		attrs[string(rune('a'+i))] = "value"
	}

	pred := &Prediction{
		Attributes: attrs,
	}

	// Should not error, but should log warning
	err = client.RecordPrediction(context.Background(), pred)
	if err != nil {
		t.Errorf("RecordPrediction() unexpected error: %v", err)
	}
}

// TestShutdown_Success verifies graceful shutdown.
func TestShutdown_Success(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}

	err = client.Shutdown()
	if err != nil {
		t.Errorf("Shutdown() unexpected error: %v", err)
	}
}

// TestShutdown_Idempotent verifies multiple shutdown calls are safe.
func TestShutdown_Idempotent(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}

	// Call shutdown multiple times
	err1 := client.Shutdown()
	err2 := client.Shutdown()
	err3 := client.Shutdown()

	if err1 != nil {
		t.Errorf("First Shutdown() unexpected error: %v", err1)
	}
	// Subsequent calls should return the same error (or nil)
	if err2 != err1 {
		t.Errorf("Second Shutdown() error = %v, want %v", err2, err1)
	}
	if err3 != err1 {
		t.Errorf("Third Shutdown() error = %v, want %v", err3, err1)
	}
}

// TestConcurrency_RecordPrediction verifies goroutine safety.
func TestConcurrency_RecordPrediction(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	const numGoroutines = 100
	const predictionsPerGoroutine = 10

	var wg sync.WaitGroup
	wg.Add(numGoroutines)

	// Launch multiple goroutines recording predictions
	for i := 0; i < numGoroutines; i++ {
		go func(id int) {
			defer wg.Done()
			for j := 0; j < predictionsPerGoroutine; j++ {
				latency := 0.10 + float64(j)*0.01
				pred := &Prediction{
					Latency:      &latency,
					PredictionID: "pred-" + string(rune(id)),
					Attributes: map[string]string{
						"goroutine": string(rune(id)),
						"iteration": string(rune(j)),
					},
				}
				if err := client.RecordPrediction(context.Background(), pred); err != nil {
					t.Errorf("RecordPrediction() error in goroutine %d: %v", id, err)
				}
			}
		}(i)
	}

	wg.Wait()
}

// TestDefaultValues verifies that default values are applied.
func TestDefaultValues(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		// Don't set optional fields - should get defaults
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	// Verify defaults were applied
	if client.config.ModelVersion != "1.0.0" {
		t.Errorf("ModelVersion = %q, want %q", client.config.ModelVersion, "1.0.0")
	}
	if client.config.ModelType != "internal" {
		t.Errorf("ModelType = %q, want %q", client.config.ModelType, "internal")
	}
	if client.config.ModelCategory != "internal" {
		t.Errorf("ModelCategory = %q, want %q", client.config.ModelCategory, "internal")
	}
	if client.config.CollectorEndpoint != "http://localhost:4318" {
		t.Errorf("CollectorEndpoint = %q, want %q", client.config.CollectorEndpoint, "http://localhost:4318")
	}
	if client.config.Protocol != "http" {
		t.Errorf("Protocol = %q, want %q", client.config.Protocol, "http")
	}
}

// TestConcurrency_ShutdownDuringRecording verifies no race between Shutdown and RecordPrediction.
func TestConcurrency_ShutdownDuringRecording(t *testing.T) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		t.Fatalf("NewClient() error: %v", err)
	}

	// Launch goroutine that continuously records predictions
	var wg sync.WaitGroup
	wg.Add(1)
	stopRecording := make(chan struct{})

	go func() {
		defer wg.Done()
		latency := 0.10
		for {
			select {
			case <-stopRecording:
				return
			default:
				pred := &Prediction{
					Latency:      &latency,
					PredictionID: "pred-concurrent",
				}
				// This should either succeed or return ErrClientClosed, never panic
				_ = client.RecordPrediction(context.Background(), pred)
				time.Sleep(1 * time.Millisecond) // Small delay to allow shutdown to happen
			}
		}
	}()

	// Give recording goroutine time to start
	time.Sleep(10 * time.Millisecond)

	// Shutdown client while recording is happening
	if err := client.Shutdown(); err != nil {
		t.Logf("Shutdown() error (acceptable): %v", err)
	}

	// Stop recording goroutine
	close(stopRecording)
	wg.Wait()

	// Verify subsequent calls return ErrClientClosed
	pred := &Prediction{PredictionID: "pred-after-race"}
	err = client.RecordPrediction(context.Background(), pred)
	if err != ErrClientClosed {
		t.Errorf("RecordPrediction() after race: got %v, want %v", err, ErrClientClosed)
	}
}

// BenchmarkRecordPrediction measures prediction recording performance.
func BenchmarkRecordPrediction(b *testing.B) {
	cfg := &config.Config{
		ServiceName: "test-service",
		ModelID:     "mdl-001",
		TeamName:    "test-team",
		TLSInsecure: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	client, err := NewClient(ctx, cfg)
	if err != nil {
		b.Fatalf("NewClient() error: %v", err)
	}
	defer client.Shutdown()

	latency := 0.15
	pred := &Prediction{
		Latency:      &latency,
		PredictionID: "pred-bench",
		Attributes: map[string]string{
			"model_version": "2.0",
		},
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = client.RecordPrediction(context.Background(), pred)
	}
}
