package sdk

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"fmt"
	"testing"
	"time"

	"log/slog"
	"os"

	"github.com/nats-io/nats.go"
)

var testCounter int

// generateUniqueAgentID generates unique agent IDs for tests to avoid expvar conflicts
func generateUniqueAgentID(prefix string) string {
	testCounter++
	return fmt.Sprintf("%s-%d", prefix, testCounter)
}

// TestClient tests basic client creation and configuration
func TestClient(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")
	config.NATSUrl = "nats://localhost:4222"

	client := NewClient(config)
	if client == nil {
		t.Fatal("Expected client to be created")
	}

	// Test health before start
	health := client.Health()
	if health.Connected {
		t.Error("Expected client to be disconnected before start")
	}
}

// TestBeatCallbacks tests beat and downbeat callback registration
func TestBeatCallbacks(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent-callbacks")

	client := NewClient(config)

	var beatCalled, downbeatCalled bool

	// Register callbacks
	err := client.OnBeat(func(beat BeatFrame) {
		beatCalled = true
	})
	if err != nil {
		t.Fatalf("Failed to register beat callback: %v", err)
	}

	err = client.OnDownbeat(func(beat BeatFrame) {
		downbeatCalled = true
	})
	if err != nil {
		t.Fatalf("Failed to register downbeat callback: %v", err)
	}

	// Test nil callback rejection
	err = client.OnBeat(nil)
	if err == nil {
		t.Error("Expected error when registering nil beat callback")
	}

	err = client.OnDownbeat(nil)
	if err == nil {
		t.Error("Expected error when registering nil downbeat callback")
	}
	
	// Use variables to prevent unused warnings  
	_ = beatCalled
	_ = downbeatCalled
}

// TestStatusClaim tests status claim validation and emission
func TestStatusClaim(t *testing.T) {
	_, signingKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("Failed to generate signing key: %v", err)
	}

	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")
	config.SigningKey = signingKey

	client := NewClient(config).(*client)

	// Test valid status claim
	claim := StatusClaim{
		State:     "executing",
		BeatsLeft: 5,
		Progress:  0.5,
		Notes:     "Test status",
	}

	// Test validation without connection (should work for validation)
	client.currentBeat = 1
	client.currentHLC = "test-hlc"

	// Test auto-population
	if claim.AgentID != "" {
		t.Error("Expected AgentID to be empty before emission")
	}

	// Since we can't actually emit without NATS connection, test validation directly
	claim.Type = "backbeat.statusclaim.v1"
	claim.AgentID = config.AgentID
	claim.TaskID = "test-task"
	claim.BeatIndex = 1
	claim.HLC = "test-hlc"

	err = client.validateStatusClaim(&claim)
	if err != nil {
		t.Errorf("Expected valid status claim to pass validation: %v", err)
	}

	// Test invalid states
	invalidClaim := claim
	invalidClaim.State = "invalid-state"
	err = client.validateStatusClaim(&invalidClaim)
	if err == nil {
		t.Error("Expected invalid state to fail validation")
	}

	// Test invalid progress
	invalidClaim = claim
	invalidClaim.Progress = 1.5
	err = client.validateStatusClaim(&invalidClaim)
	if err == nil {
		t.Error("Expected invalid progress to fail validation")
	}

	// Test negative beats left
	invalidClaim = claim
	invalidClaim.BeatsLeft = -1
	err = client.validateStatusClaim(&invalidClaim)
	if err == nil {
		t.Error("Expected negative beats_left to fail validation")
	}
}

// TestBeatBudget tests beat budget functionality
func TestBeatBudget(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")

	client := NewClient(config).(*client)
	client.currentTempo = 120 // 120 BPM = 0.5 seconds per beat

	ctx := context.Background()
	client.ctx = ctx

	// Test successful execution within budget
	executed := false
	err := client.WithBeatBudget(2, func() error {
		executed = true
		time.Sleep(100 * time.Millisecond) // Much less than 2 beats (1 second)
		return nil
	})

	if err != nil {
		t.Errorf("Expected function to complete successfully: %v", err)
	}
	if !executed {
		t.Error("Expected function to be executed")
	}

	// Test timeout (need to be careful with timing)
	timeoutErr := client.WithBeatBudget(1, func() error {
		time.Sleep(2 * time.Second) // More than 1 beat at 120 BPM (0.5s)
		return nil
	})

	if timeoutErr == nil {
		t.Error("Expected function to timeout")
	}
	if timeoutErr.Error() != "beat budget of 1 beats exceeded" {
		t.Errorf("Expected timeout error message, got: %v", timeoutErr)
	}

	// Test invalid budget
	err = client.WithBeatBudget(0, func() error { return nil })
	if err == nil {
		t.Error("Expected error for zero beat budget")
	}

	err = client.WithBeatBudget(-1, func() error { return nil })
	if err == nil {
		t.Error("Expected error for negative beat budget")
	}
}

// TestTempoTracking tests tempo tracking and drift calculation
func TestTempoTracking(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")

	client := NewClient(config).(*client)

	// Test initial values
	if client.GetCurrentTempo() != 60 {
		t.Errorf("Expected default tempo to be 60, got %d", client.GetCurrentTempo())
	}

	if client.GetTempoDrift() != 0 {
		t.Errorf("Expected initial tempo drift to be 0, got %v", client.GetTempoDrift())
	}

	// Simulate tempo changes
	client.beatMutex.Lock()
	client.currentTempo = 120
	client.tempoHistory = append(client.tempoHistory, tempoSample{
		BeatIndex:    1,
		Tempo:        120,
		MeasuredTime: time.Now(),
		ActualBPM:    118.0, // Slightly slower than expected
	})
	client.tempoHistory = append(client.tempoHistory, tempoSample{
		BeatIndex:    2,
		Tempo:        120,
		MeasuredTime: time.Now().Add(500 * time.Millisecond),
		ActualBPM:    119.0, // Still slightly slower
	})
	client.beatMutex.Unlock()

	if client.GetCurrentTempo() != 2 {
		t.Errorf("Expected current tempo to be 2 BPM (30s beats), got %d", client.GetCurrentTempo())
	}

	// Test drift calculation (should be non-zero due to difference between 120 and measured BPM)
	drift := client.GetTempoDrift()
	if drift == 0 {
		t.Error("Expected non-zero tempo drift")
	}
}

// TestLegacyCompatibility tests legacy beat conversion
func TestLegacyCompatibility(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")

	client := NewClient(config).(*client)

	// Test legacy beat conversion
	beatIndex := client.ConvertLegacyBeat(2, 3) // Bar 2, Beat 3
	expectedBeatIndex := int64(7)               // (2-1)*4 + 3 = 7
	if beatIndex != expectedBeatIndex {
		t.Errorf("Expected beat index %d, got %d", expectedBeatIndex, beatIndex)
	}

	// Test reverse conversion
	client.beatMutex.Lock()
	client.currentBeat = 7
	client.beatMutex.Unlock()

	legacyInfo := client.GetLegacyBeatInfo()
	if legacyInfo.Bar != 2 || legacyInfo.Beat != 3 {
		t.Errorf("Expected bar=2, beat=3, got bar=%d, beat=%d", legacyInfo.Bar, legacyInfo.Beat)
	}

	// Test edge cases
	beatIndex = client.ConvertLegacyBeat(1, 1) // First beat
	if beatIndex != 1 {
		t.Errorf("Expected beat index 1 for first beat, got %d", beatIndex)
	}

	client.beatMutex.Lock()
	client.currentBeat = 0 // Edge case
	client.beatMutex.Unlock()

	legacyInfo = client.GetLegacyBeatInfo()
	if legacyInfo.Bar != 1 || legacyInfo.Beat != 1 {
		t.Errorf("Expected bar=1, beat=1 for zero beat, got bar=%d, beat=%d", legacyInfo.Bar, legacyInfo.Beat)
	}
}

// TestHealthStatus tests health status reporting
func TestHealthStatus(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")

	client := NewClient(config).(*client)

	// Test initial health
	health := client.Health()
	if health.Connected {
		t.Error("Expected client to be disconnected initially")
	}
	if health.LastBeat != 0 {
		t.Error("Expected last beat to be 0 initially")
	}
	if health.CurrentTempo != 60 {
		t.Errorf("Expected default tempo 60, got %d", health.CurrentTempo)
	}

	// Simulate some activity
	client.beatMutex.Lock()
	client.currentBeat = 10
	client.currentTempo = 90
	client.lastBeatTime = time.Now().Add(-100 * time.Millisecond)
	client.beatMutex.Unlock()

	client.addError("test error")

	health = client.Health()
	if health.LastBeat != 10 {
		t.Errorf("Expected last beat to be 10, got %d", health.LastBeat)
	}
	if health.CurrentTempo != 90 {
		t.Errorf("Expected current tempo to be 90, got %d", health.CurrentTempo)
	}
	if len(health.Errors) != 1 {
		t.Errorf("Expected 1 error, got %d", len(health.Errors))
	}
	if health.TimeDrift <= 0 {
		t.Error("Expected positive time drift")
	}
}

// TestMetrics tests metrics integration
func TestMetrics(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")

	client := NewClient(config).(*client)

	if client.metrics == nil {
		t.Fatal("Expected metrics to be initialized")
	}

	// Test metrics snapshot
	snapshot := client.metrics.GetMetricsSnapshot()
	if snapshot == nil {
		t.Error("Expected metrics snapshot to be available")
	}

	// Check for expected metric keys
	expectedKeys := []string{
		"connection_status",
		"reconnect_count",
		"beats_received",
		"status_claims_emitted",
		"budgets_created",
		"total_errors",
	}

	for _, key := range expectedKeys {
		if _, exists := snapshot[key]; !exists {
			t.Errorf("Expected metric key '%s' to exist in snapshot", key)
		}
	}
}

// TestConfig tests configuration validation and defaults
func TestConfig(t *testing.T) {
	// Test default config
	config := DefaultConfig()
	if config.JitterTolerance != 50*time.Millisecond {
		t.Errorf("Expected default jitter tolerance 50ms, got %v", config.JitterTolerance)
	}
	if config.ReconnectDelay != 1*time.Second {
		t.Errorf("Expected default reconnect delay 1s, got %v", config.ReconnectDelay)
	}
	if config.MaxReconnects != -1 {
		t.Errorf("Expected default max reconnects -1, got %d", config.MaxReconnects)
	}

	// Test logger initialization
	config.Logger = nil
	client := NewClient(config)
	if client == nil {
		t.Error("Expected client to be created even with nil logger")
	}

	// Test with custom config
	_, signingKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("Failed to generate signing key: %v", err)
	}

	config.ClusterID = "custom-cluster"
	config.AgentID = "custom-agent"
	config.SigningKey = signingKey
	config.JitterTolerance = 100 * time.Millisecond
	config.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))

	client = NewClient(config)
	if client == nil {
		t.Error("Expected client to be created with custom config")
	}
}

// TestBeatDurationCalculation tests beat duration calculation
func TestBeatDurationCalculation(t *testing.T) {
	config := DefaultConfig()
	config.ClusterID = "test-cluster"
	config.AgentID = generateUniqueAgentID("test-agent")

	client := NewClient(config).(*client)

	// Test default 60 BPM (1 second per beat)
	duration := client.getBeatDuration()
	expected := 1000 * time.Millisecond
	if duration != expected {
		t.Errorf("Expected beat duration %v for 60 BPM, got %v", expected, duration)
	}

	// Test 120 BPM (0.5 seconds per beat)
	client.beatMutex.Lock()
	client.currentTempo = 120
	client.beatMutex.Unlock()

	duration = client.getBeatDuration()
	expected = 500 * time.Millisecond
	if duration != expected {
		t.Errorf("Expected beat duration %v for 120 BPM, got %v", expected, duration)
	}

	// Test 30 BPM (2 seconds per beat)
	client.beatMutex.Lock()
	client.currentTempo = 30
	client.beatMutex.Unlock()

	duration = client.getBeatDuration()
	expected = 2000 * time.Millisecond
	if duration != expected {
		t.Errorf("Expected beat duration %v for 30 BPM, got %v", expected, duration)
	}

	// Test edge case: zero tempo (should default to 60 BPM)
	client.beatMutex.Lock()
	client.currentTempo = 0
	client.beatMutex.Unlock()

	duration = client.getBeatDuration()
	expected = 1000 * time.Millisecond
	if duration != expected {
		t.Errorf("Expected beat duration %v for 0 BPM (default 60), got %v", expected, duration)
	}
}

// BenchmarkBeatCallback benchmarks beat callback execution
func BenchmarkBeatCallback(b *testing.B) {
	config := DefaultConfig()
	config.ClusterID = "bench-cluster"
	config.AgentID = "bench-agent"

	client := NewClient(config).(*client)

	beatFrame := BeatFrame{
		Type:       "backbeat.beatframe.v1",
		ClusterID:  "bench-cluster",
		BeatIndex:  1,
		Downbeat:   false,
		Phase:      "test",
		HLC:        "test-hlc",
		DeadlineAt: time.Now().Add(time.Second),
		TempoBPM:   60,
		WindowID:   "test-window",
	}

	callbackCount := 0
	client.OnBeat(func(beat BeatFrame) {
		callbackCount++
	})

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		client.safeExecuteCallback(client.beatCallbacks[0], beatFrame, "beat")
	}

	if callbackCount != b.N {
		b.Errorf("Expected callback to be called %d times, got %d", b.N, callbackCount)
	}
}

// BenchmarkStatusClaimValidation benchmarks status claim validation
func BenchmarkStatusClaimValidation(b *testing.B) {
	config := DefaultConfig()
	config.ClusterID = "bench-cluster"
	config.AgentID = "bench-agent"

	client := NewClient(config).(*client)

	claim := StatusClaim{
		Type:      "backbeat.statusclaim.v1",
		AgentID:   "bench-agent",
		TaskID:    "bench-task",
		BeatIndex: 1,
		State:     "executing",
		BeatsLeft: 5,
		Progress:  0.5,
		Notes:     "Benchmark test",
		HLC:       "bench-hlc",
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		err := client.validateStatusClaim(&claim)
		if err != nil {
			b.Fatal(err)
		}
	}
}

// Mock NATS server for integration tests (if needed)
func setupTestNATSServer(t *testing.T) *nats.Conn {
	// This would start an embedded NATS server for testing
	// For now, we'll skip tests that require NATS if it's not available
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		t.Skipf("NATS server not available: %v", err)
		return nil
	}
	return nc
}

func TestIntegrationWithNATS(t *testing.T) {
	nc := setupTestNATSServer(t)
	if nc == nil {
		return // Skipped
	}
	defer nc.Close()

	config := DefaultConfig()
	config.ClusterID = "integration-test"
	config.AgentID = generateUniqueAgentID("test-agent")
	config.NATSUrl = nats.DefaultURL

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

	// Test start/stop cycle
	err := client.Start(ctx)
	if err != nil {
		t.Fatalf("Failed to start client: %v", err)
	}

	// Check health after start
	health := client.Health()
	if !health.Connected {
		t.Error("Expected client to be connected after start")
	}

	// Test stop
	err = client.Stop()
	if err != nil {
		t.Errorf("Failed to stop client: %v", err)
	}

	// Check health after stop
	health = client.Health()
	if health.Connected {
		t.Error("Expected client to be disconnected after stop")
	}
}