package traefikoidc

import (
	"context"
	"runtime"
	"sync"
	"testing"
	"time"
)

// TestGoroutineLeakPrevention_ContextCancellation tests that goroutines are properly cleaned up
// when the context is canceled during middleware initialization and operation
func TestGoroutineLeakPrevention_ContextCancellation(t *testing.T) {
	tests := []struct {
		name          string
		description   string
		cancelAfter   time.Duration
		expectedLeaks int
	}{
		{
			name:          "immediate_cancellation",
			cancelAfter:   1 * time.Millisecond,
			expectedLeaks: 10, // Allow for background tasks (replay-cache-cleanup, health-check, etc.)
			description:   "Context canceled immediately during initialization",
		},
		{
			name:          "quick_cancellation",
			cancelAfter:   50 * time.Millisecond,
			expectedLeaks: 5, // Allow for some background task leaks during cancellation
			description:   "Context canceled during metadata initialization",
		},
		{
			name:          "delayed_cancellation",
			cancelAfter:   200 * time.Millisecond,
			expectedLeaks: 5, // Allow for some background task leaks during cancellation
			description:   "Context canceled after partial initialization",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Record initial goroutine count
			runtime.GC()
			runtime.GC() // Double GC to ensure cleanup
			time.Sleep(10 * time.Millisecond)
			initialGoroutines := runtime.NumGoroutine()

			// Create cancellable context
			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			// Create plugin config
			config := CreateConfig()
			config.ProviderURL = "https://accounts.google.com"
			config.SessionEncryptionKey = "test-encryption-key-32-bytes-long"
			config.ClientID = "test-client-id"
			config.ClientSecret = "test-client-secret"

			// Start goroutine leak test
			var plugin *TraefikOidc
			var wg sync.WaitGroup

			// Initialize plugin in separate goroutine to simulate real usage
			wg.Add(1)
			go func() {
				defer wg.Done()
				handler, _ := New(ctx, nil, config, "test")
				if handler != nil {
					plugin = handler.(*TraefikOidc)
				}
			}()

			// Cancel context after specified delay
			time.Sleep(tt.cancelAfter)
			cancel()

			// Wait for initialization to complete or timeout
			done := make(chan struct{})
			go func() {
				wg.Wait()
				close(done)
			}()

			select {
			case <-done:
				// Initialization completed (or was canceled)
			case <-time.After(5 * time.Second):
				t.Fatal("Plugin initialization did not complete within timeout")
			}

			// Clean up plugin if it was created
			if plugin != nil {
				// Use proper Close() method for cleanup
				if err := plugin.Close(); err != nil {
					t.Logf("Plugin close error: %v", err)
				}
			}

			// Allow time for goroutine cleanup
			time.Sleep(100 * time.Millisecond)
			runtime.GC()
			runtime.GC()
			time.Sleep(50 * time.Millisecond)

			// Check final goroutine count
			finalGoroutines := runtime.NumGoroutine()
			goroutineDiff := finalGoroutines - initialGoroutines

			if goroutineDiff > tt.expectedLeaks {
				t.Errorf("Goroutine leak detected: %s\n"+
					"Initial goroutines: %d\n"+
					"Final goroutines: %d\n"+
					"Difference: %d (expected max: %d)",
					tt.description, initialGoroutines, finalGoroutines,
					goroutineDiff, tt.expectedLeaks)
			}

			t.Logf("Test %s: Initial: %d, Final: %d, Diff: %d",
				tt.name, initialGoroutines, finalGoroutines, goroutineDiff)
		})
	}
}

// TestGoroutineLeakPrevention_PanicRecovery tests that goroutines are cleaned up
// even when panics occur during initialization
func TestGoroutineLeakPrevention_PanicRecovery(t *testing.T) {
	runtime.GC()
	runtime.GC()
	time.Sleep(10 * time.Millisecond)
	initialGoroutines := runtime.NumGoroutine()

	// Create context that will be valid but cause initialization issues
	ctx := context.Background()

	// Create invalid config to potentially cause panics
	config := CreateConfig()
	config.ProviderURL = "://invalid-url"     // Invalid URL format
	config.SessionEncryptionKey = "too-short" // Invalid key length
	config.ClientID = ""
	config.ClientSecret = ""

	// Attempt to create plugin - should handle errors gracefully
	handler, err := New(ctx, nil, config, "test")
	var plugin *TraefikOidc
	if handler != nil {
		plugin = handler.(*TraefikOidc)
	}

	// Verify error is handled gracefully (no panic)
	if err == nil {
		t.Log("Plugin creation succeeded despite invalid config")
		if plugin != nil {
			// Clean up if somehow created using proper Close() method
			if err := plugin.Close(); err != nil {
				t.Logf("Plugin close error: %v", err)
			}
		}
	} else {
		t.Logf("Plugin creation failed as expected: %v", err)
	}

	// Allow cleanup time
	time.Sleep(100 * time.Millisecond)
	runtime.GC()
	runtime.GC()
	time.Sleep(50 * time.Millisecond)

	finalGoroutines := runtime.NumGoroutine()
	goroutineDiff := finalGoroutines - initialGoroutines

	if goroutineDiff > 5 { // Allow more tolerance for background tasks
		t.Errorf("Goroutine leak after panic recovery: "+
			"Initial: %d, Final: %d, Diff: %d",
			initialGoroutines, finalGoroutines, goroutineDiff)
	}
}

// TestGoroutineLeakPrevention_MultipleInstances tests that multiple middleware instances
// don't cause goroutine leaks
func TestGoroutineLeakPrevention_MultipleInstances(t *testing.T) {
	runtime.GC()
	runtime.GC()
	time.Sleep(10 * time.Millisecond)
	initialGoroutines := runtime.NumGoroutine()

	ctx := context.Background()
	const numInstances = 5
	plugins := make([]*TraefikOidc, 0, numInstances)

	// Create multiple plugin instances
	for i := 0; i < numInstances; i++ {
		config := CreateConfig()
		config.ProviderURL = "https://accounts.google.com"
		config.SessionEncryptionKey = "test-encryption-key-32-bytes-long"
		config.ClientID = "test-client-id"
		config.ClientSecret = "test-client-secret"
		config.CallbackURL = "/callback"

		handler, err := New(ctx, nil, config, "test")
		if err != nil {
			t.Fatalf("Failed to create plugin instance %d: %v", i, err)
		}
		if handler != nil {
			plugin := handler.(*TraefikOidc)
			plugins = append(plugins, plugin)
		}
	}

	// Allow initialization to complete
	time.Sleep(100 * time.Millisecond)

	// Clean up all plugins
	var wg sync.WaitGroup
	for i, plugin := range plugins {
		wg.Add(1)
		go func(p *TraefikOidc, idx int) {
			defer wg.Done()
			// Use proper Close() method for cleanup
			if err := p.Close(); err != nil {
				t.Logf("Plugin %d close error: %v", idx, err)
			}
		}(plugin, i)
	}

	// Wait for all cleanups with timeout
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		// All cleanups completed
	case <-time.After(10 * time.Second):
		t.Fatal("Plugin cleanup did not complete within timeout")
	}

	// Allow final cleanup
	time.Sleep(200 * time.Millisecond)
	runtime.GC()
	runtime.GC()
	time.Sleep(100 * time.Millisecond)

	finalGoroutines := runtime.NumGoroutine()
	goroutineDiff := finalGoroutines - initialGoroutines

	// Allow for reasonable tolerance due to background tasks and test infrastructure
	maxExpectedLeaks := 10 // Increased to account for background tasks from multiple instances
	if goroutineDiff > maxExpectedLeaks {
		t.Errorf("Excessive goroutine leaks with multiple instances: "+
			"Initial: %d, Final: %d, Diff: %d (max expected: %d)",
			initialGoroutines, finalGoroutines, goroutineDiff, maxExpectedLeaks)
	}

	t.Logf("Multiple instances test: Created %d instances, "+
		"Initial goroutines: %d, Final: %d, Diff: %d",
		numInstances, initialGoroutines, finalGoroutines, goroutineDiff)
}

// TestGoroutineLeakPrevention_TimeoutCleanup tests that stuck goroutines are cleaned up
// within reasonable timeouts
func TestGoroutineLeakPrevention_TimeoutCleanup(t *testing.T) {
	runtime.GC()
	runtime.GC()
	time.Sleep(10 * time.Millisecond)
	initialGoroutines := runtime.NumGoroutine()

	// Create context with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	config := CreateConfig()
	config.ProviderURL = "https://httpbin.org/delay/10" // Slow endpoint to trigger timeout
	config.SessionEncryptionKey = "test-encryption-key-32-bytes-long"
	config.ClientID = "test-client-id"
	config.ClientSecret = "test-client-secret"

	// Create plugin - initialization may timeout
	handler, err := New(ctx, nil, config, "test")
	var plugin *TraefikOidc
	if handler != nil {
		plugin = handler.(*TraefikOidc)
	}

	// Wait for context timeout
	<-ctx.Done()

	if plugin != nil {
		// Clean up if plugin was created using proper Close() method
		if err := plugin.Close(); err != nil {
			t.Logf("Plugin close error: %v", err)
		}
	}

	// Allow extended cleanup time for timeout scenarios
	time.Sleep(300 * time.Millisecond)
	runtime.GC()
	runtime.GC()
	time.Sleep(100 * time.Millisecond)

	finalGoroutines := runtime.NumGoroutine()
	goroutineDiff := finalGoroutines - initialGoroutines

	if goroutineDiff > 5 { // Allow more tolerance for timeout scenarios
		t.Errorf("Goroutines not cleaned up after timeout: "+
			"Initial: %d, Final: %d, Diff: %d, Error: %v",
			initialGoroutines, finalGoroutines, goroutineDiff, err)
	}
}

// TestGoroutineLeakPrevention_BackgroundTaskCleanup tests that background metadata refresh
// goroutines are properly stopped and cleaned up
func TestGoroutineLeakPrevention_BackgroundTaskCleanup(t *testing.T) {
	runtime.GC()
	runtime.GC()
	time.Sleep(10 * time.Millisecond)
	initialGoroutines := runtime.NumGoroutine()

	ctx := context.Background()
	config := CreateConfig()
	config.ProviderURL = "https://accounts.google.com"
	config.SessionEncryptionKey = "test-encryption-key-32-bytes-long"
	config.ClientID = "test-client-id"
	config.ClientSecret = "test-client-secret"
	config.CallbackURL = "/callback"

	handler, err := New(ctx, nil, config, "test")
	if err != nil {
		t.Fatalf("Failed to create plugin: %v", err)
	}
	plugin := handler.(*TraefikOidc)

	// Allow initialization and background task startup
	time.Sleep(200 * time.Millisecond)

	// Check that we have more goroutines (background tasks started)
	midGoroutines := runtime.NumGoroutine()
	if midGoroutines <= initialGoroutines {
		t.Log("Warning: No additional goroutines detected for background tasks")
	}

	// Stop all background tasks properly
	err = plugin.Close()
	if err != nil {
		t.Logf("Warning: Error closing plugin: %v", err)
	}

	// Allow cleanup time
	time.Sleep(200 * time.Millisecond)
	runtime.GC()
	runtime.GC()
	time.Sleep(100 * time.Millisecond)

	finalGoroutines := runtime.NumGoroutine()
	goroutineDiff := finalGoroutines - initialGoroutines

	if goroutineDiff > 5 { // Allow tolerance for background task cleanup timing
		t.Errorf("Background tasks not properly cleaned up: "+
			"Initial: %d, Mid: %d, Final: %d, Diff: %d",
			initialGoroutines, midGoroutines, finalGoroutines, goroutineDiff)
	}

	t.Logf("Background task cleanup: Initial: %d, Mid: %d, Final: %d",
		initialGoroutines, midGoroutines, finalGoroutines)
}

// BenchmarkGoroutineLeakPrevention_CreationDestruction benchmarks goroutine usage
// during plugin creation and destruction cycles
func BenchmarkGoroutineLeakPrevention_CreationDestruction(b *testing.B) {
	ctx := context.Background()

	// Record baseline
	runtime.GC()
	runtime.GC()
	time.Sleep(10 * time.Millisecond)
	baselineGoroutines := runtime.NumGoroutine()

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		config := CreateConfig()
		config.ProviderURL = "https://accounts.google.com"
		config.SessionEncryptionKey = "test-encryption-key-32-bytes-long"
		config.ClientID = "test-client-id"
		config.ClientSecret = "test-client-secret"

		handler, err := New(ctx, nil, config, "test")
		if err != nil {
			b.Fatalf("Failed to create plugin: %v", err)
		}
		plugin := handler.(*TraefikOidc)

		// Clean up immediately using proper Close() method
		if err := plugin.Close(); err != nil {
			b.Logf("Plugin close error at iteration %d: %v", i, err)
		}

		// Periodic goroutine count check
		if i%100 == 99 {
			runtime.GC()
			current := runtime.NumGoroutine()
			if current > baselineGoroutines+10 {
				b.Fatalf("Goroutine leak detected at iteration %d: baseline=%d, current=%d",
					i, baselineGoroutines, current)
			}
		}
	}

	b.StopTimer()

	// Final cleanup and verification
	runtime.GC()
	runtime.GC()
	time.Sleep(50 * time.Millisecond)
	finalGoroutines := runtime.NumGoroutine()

	if finalGoroutines > baselineGoroutines+5 {
		b.Errorf("Potential goroutine leak after benchmark: baseline=%d, final=%d",
			baselineGoroutines, finalGoroutines)
	}
}
