package providers

import (
	"fmt"
	"sync"
	"testing"
)

// TestProviderRegistry_NewProviderRegistry tests registry constructor
func TestProviderRegistry_NewProviderRegistry(t *testing.T) {
	registry := NewProviderRegistry()

	if registry == nil {
		t.Fatal("Expected registry to be created, got nil")
	}

	if registry.providers == nil {
		t.Error("Providers slice should be initialized")
	}

	if registry.cache == nil {
		t.Error("Cache map should be initialized")
	}

	if registry.typeMap == nil {
		t.Error("TypeMap should be initialized")
	}

	if registry.maxCacheSize != 1000 {
		t.Errorf("Expected maxCacheSize 1000, got %d", registry.maxCacheSize)
	}

	if registry.cacheCount != 0 {
		t.Errorf("Expected initial cacheCount 0, got %d", registry.cacheCount)
	}
}

// TestProviderRegistry_RegisterProvider tests provider registration
func TestProviderRegistry_RegisterProvider(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	googleProvider := NewGoogleProvider()
	azureProvider := NewAzureProvider()

	// Register providers
	registry.RegisterProvider(genericProvider)
	registry.RegisterProvider(googleProvider)
	registry.RegisterProvider(azureProvider)

	// Verify providers are registered
	if len(registry.providers) != 3 {
		t.Errorf("Expected 3 providers, got %d", len(registry.providers))
	}

	if len(registry.typeMap) != 3 {
		t.Errorf("Expected 3 type mappings, got %d", len(registry.typeMap))
	}

	// Verify type mappings
	if registry.typeMap[ProviderTypeGeneric] != genericProvider {
		t.Error("Generic provider not mapped correctly")
	}

	if registry.typeMap[ProviderTypeGoogle] != googleProvider {
		t.Error("Google provider not mapped correctly")
	}

	if registry.typeMap[ProviderTypeAzure] != azureProvider {
		t.Error("Azure provider not mapped correctly")
	}
}

// TestProviderRegistry_GetProviderByType tests provider retrieval by type
func TestProviderRegistry_GetProviderByType(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	googleProvider := NewGoogleProvider()

	registry.RegisterProvider(genericProvider)
	registry.RegisterProvider(googleProvider)

	tests := []struct {
		expected     OIDCProvider
		name         string
		providerType ProviderType
	}{
		{
			name:         "Get Generic provider",
			providerType: ProviderTypeGeneric,
			expected:     genericProvider,
		},
		{
			name:         "Get Google provider",
			providerType: ProviderTypeGoogle,
			expected:     googleProvider,
		},
		{
			name:         "Get unregistered provider",
			providerType: ProviderTypeAzure,
			expected:     nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := registry.GetProviderByType(tt.providerType)

			if result != tt.expected {
				t.Errorf("Expected %v, got %v", tt.expected, result)
			}
		})
	}
}

// TestProviderRegistry_GetRegisteredProviders tests listing registered provider types
func TestProviderRegistry_GetRegisteredProviders(t *testing.T) {
	registry := NewProviderRegistry()

	// Initially empty
	types := registry.GetRegisteredProviders()
	if len(types) != 0 {
		t.Errorf("Expected 0 registered providers, got %d", len(types))
	}

	// Register some providers
	registry.RegisterProvider(NewGenericProvider())
	registry.RegisterProvider(NewGoogleProvider())

	types = registry.GetRegisteredProviders()
	if len(types) != 2 {
		t.Errorf("Expected 2 registered providers, got %d", len(types))
	}

	// Verify types are correct
	expectedTypes := map[ProviderType]bool{
		ProviderTypeGeneric: false,
		ProviderTypeGoogle:  false,
	}

	for _, providerType := range types {
		if _, exists := expectedTypes[providerType]; exists {
			expectedTypes[providerType] = true
		} else {
			t.Errorf("Unexpected provider type: %v", providerType)
		}
	}

	for providerType, found := range expectedTypes {
		if !found {
			t.Errorf("Provider type %v not found in results", providerType)
		}
	}
}

// TestProviderRegistry_DetectProvider tests provider detection
func TestProviderRegistry_DetectProvider(t *testing.T) {
	registry := NewProviderRegistry()

	// Register providers
	genericProvider := NewGenericProvider()
	googleProvider := NewGoogleProvider()
	azureProvider := NewAzureProvider()
	githubProvider := NewGitHubProvider()
	auth0Provider := NewAuth0Provider()
	oktaProvider := NewOktaProvider()
	keycloakProvider := NewKeycloakProvider()
	cognitoProvider := NewAWSCognitoProvider()
	gitlabProvider := NewGitLabProvider()

	registry.RegisterProvider(genericProvider)
	registry.RegisterProvider(googleProvider)
	registry.RegisterProvider(azureProvider)
	registry.RegisterProvider(githubProvider)
	registry.RegisterProvider(auth0Provider)
	registry.RegisterProvider(oktaProvider)
	registry.RegisterProvider(keycloakProvider)
	registry.RegisterProvider(cognitoProvider)
	registry.RegisterProvider(gitlabProvider)

	tests := []struct {
		expected  OIDCProvider
		name      string
		issuerURL string
	}{
		{
			name:      "Google provider detection",
			issuerURL: "https://accounts.google.com",
			expected:  googleProvider,
		},
		{
			name:      "Google provider with path",
			issuerURL: "https://accounts.google.com/oauth2",
			expected:  googleProvider,
		},
		{
			name:      "Azure provider detection - login.microsoftonline.com",
			issuerURL: "https://login.microsoftonline.com/tenant/v2.0",
			expected:  azureProvider,
		},
		{
			name:      "Azure provider detection - sts.windows.net",
			issuerURL: "https://sts.windows.net/tenant",
			expected:  azureProvider,
		},
		{
			name:      "GitHub provider detection",
			issuerURL: "https://github.com/login/oauth",
			expected:  githubProvider,
		},
		{
			name:      "Auth0 provider detection",
			issuerURL: "https://tenant.auth0.com",
			expected:  auth0Provider,
		},
		{
			name:      "Okta provider detection",
			issuerURL: "https://tenant.okta.com",
			expected:  oktaProvider,
		},
		{
			name:      "Okta preview provider detection",
			issuerURL: "https://tenant.oktapreview.com",
			expected:  oktaProvider,
		},
		{
			name:      "Keycloak provider detection (legacy /auth/realms/)",
			issuerURL: "https://auth.example.com/auth/realms/master",
			expected:  keycloakProvider,
		},
		{
			name:      "Keycloak provider detection (modern /realms/, KC 17+)",
			issuerURL: "https://auth.example.com/realms/master",
			expected:  keycloakProvider,
		},
		{
			name:      "AWS Cognito provider detection",
			issuerURL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example",
			expected:  cognitoProvider,
		},
		{
			name:      "GitLab provider detection",
			issuerURL: "https://gitlab.com/oauth",
			expected:  gitlabProvider,
		},
		{
			name:      "GitLab self-hosted detection - gitlab subdomain",
			issuerURL: "https://gitlab.example.com",
			expected:  gitlabProvider,
		},
		{
			name:      "GitLab self-hosted detection - gitlab in domain",
			issuerURL: "https://my-gitlab.company.io",
			expected:  gitlabProvider,
		},
		{
			name:      "GitLab self-hosted detection - gitlab prefix",
			issuerURL: "https://gitlab-prod.internal.net",
			expected:  gitlabProvider,
		},
		{
			name:      "GitLab self-hosted detection - gitlab suffix",
			issuerURL: "https://company-gitlab.net",
			expected:  gitlabProvider,
		},
		{
			name:      "Generic provider fallback",
			issuerURL: "https://auth.example.com",
			expected:  genericProvider,
		},
		{
			name:      "Invalid URL",
			issuerURL: "not-a-url",
			expected:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := registry.DetectProvider(tt.issuerURL)

			if result != tt.expected {
				t.Errorf("Expected %v, got %v", tt.expected, result)
			}
		})
	}
}

// TestProviderRegistry_DetectProvider_Caching tests cache behavior
func TestProviderRegistry_DetectProvider_Caching(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	registry.RegisterProvider(genericProvider)

	issuerURL := "https://auth.example.com"

	// First call should detect and cache
	result1 := registry.DetectProvider(issuerURL)
	if result1 != genericProvider {
		t.Errorf("Expected generic provider, got %v", result1)
	}

	// Verify it's cached
	registry.mu.RLock()
	cachedResult, found := registry.cache[issuerURL]
	registry.mu.RUnlock()

	if !found {
		t.Error("Expected result to be cached")
	}

	if cachedResult != genericProvider {
		t.Errorf("Expected cached generic provider, got %v", cachedResult)
	}

	// Second call should return cached result
	result2 := registry.DetectProvider(issuerURL)
	if result2 != genericProvider {
		t.Errorf("Expected cached generic provider, got %v", result2)
	}

	// Should be same instance (from cache)
	if result1 != result2 {
		t.Error("Expected same instance from cache")
	}
}

// TestProviderRegistry_ClearCache tests cache clearing
func TestProviderRegistry_ClearCache(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	registry.RegisterProvider(genericProvider)

	// Populate cache
	registry.DetectProvider("https://auth1.example.com")
	registry.DetectProvider("https://auth2.example.com")

	// Verify cache has entries
	registry.mu.RLock()
	cacheSize := len(registry.cache)
	registry.mu.RUnlock()

	if cacheSize != 2 {
		t.Errorf("Expected 2 cache entries, got %d", cacheSize)
	}

	// Clear cache
	registry.ClearCache()

	// Verify cache is empty
	registry.mu.RLock()
	cacheSize = len(registry.cache)
	cacheCount := registry.cacheCount
	registry.mu.RUnlock()

	if cacheSize != 0 {
		t.Errorf("Expected 0 cache entries after clear, got %d", cacheSize)
	}

	if cacheCount != 0 {
		t.Errorf("Expected 0 cache count after clear, got %d", cacheCount)
	}
}

// TestProviderRegistry_CacheEviction tests cache size limits and eviction
func TestProviderRegistry_CacheEviction(t *testing.T) {
	registry := NewProviderRegistry()
	registry.maxCacheSize = 2 // Set small cache size for testing

	genericProvider := NewGenericProvider()
	registry.RegisterProvider(genericProvider)

	// Fill cache to capacity
	registry.DetectProvider("https://auth1.example.com")
	registry.DetectProvider("https://auth2.example.com")

	// Verify cache is at capacity
	registry.mu.RLock()
	cacheSize := len(registry.cache)
	registry.mu.RUnlock()

	if cacheSize != 2 {
		t.Errorf("Expected 2 cache entries, got %d", cacheSize)
	}

	// Add one more entry (should trigger eviction)
	registry.DetectProvider("https://auth3.example.com")

	// Cache size should still be at max
	registry.mu.RLock()
	cacheSize = len(registry.cache)
	registry.mu.RUnlock()

	if cacheSize != 2 {
		t.Errorf("Expected 2 cache entries after eviction, got %d", cacheSize)
	}

	// Verify the new entry is cached
	registry.mu.RLock()
	_, found := registry.cache["https://auth3.example.com"]
	registry.mu.RUnlock()

	if !found {
		t.Error("Expected new entry to be cached")
	}
}

// TestProviderRegistry_ConcurrentAccess tests thread safety
func TestProviderRegistry_ConcurrentAccess(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	googleProvider := NewGoogleProvider()
	azureProvider := NewAzureProvider()

	registry.RegisterProvider(genericProvider)
	registry.RegisterProvider(googleProvider)
	registry.RegisterProvider(azureProvider)

	var wg sync.WaitGroup
	goroutines := 10
	iterations := 100

	// Test concurrent detection
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				issuerURL := "https://accounts.google.com"
				if id%2 == 0 {
					issuerURL = "https://login.microsoftonline.com/tenant"
				} else if id%3 == 0 {
					issuerURL = "https://auth.example.com"
				}

				result := registry.DetectProvider(issuerURL)
				if result == nil {
					t.Errorf("Expected provider for URL %s", issuerURL)
				}
			}
		}(i)
	}

	// Test concurrent registration
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			newProvider := NewGenericProvider()
			registry.RegisterProvider(newProvider)
		}
	}()

	// Test concurrent cache clearing
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			registry.ClearCache()
		}
	}()

	wg.Wait()

	// Verify final state is consistent
	types := registry.GetRegisteredProviders()
	if len(types) < 3 { // Should have at least the original 3
		t.Errorf("Expected at least 3 provider types, got %d", len(types))
	}
}

// TestProviderRegistry_DoubleCheckedLocking tests the double-checked locking pattern
func TestProviderRegistry_DoubleCheckedLocking(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	registry.RegisterProvider(genericProvider)

	var wg sync.WaitGroup
	goroutines := 100
	issuerURL := "https://auth.example.com"

	// Multiple goroutines trying to detect the same provider simultaneously
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			result := registry.DetectProvider(issuerURL)
			if result != genericProvider {
				t.Errorf("Expected generic provider, got %v", result)
			}
		}()
	}

	wg.Wait()

	// Verify only one cache entry was created
	registry.mu.RLock()
	cacheSize := len(registry.cache)
	registry.mu.RUnlock()

	if cacheSize != 1 {
		t.Errorf("Expected 1 cache entry, got %d", cacheSize)
	}
}

// TestProviderRegistry_DetectGitLabSelfHosted tests improved GitLab detection for issue #61
func TestProviderRegistry_DetectGitLabSelfHosted(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	gitlabProvider := NewGitLabProvider()
	githubProvider := NewGitHubProvider()

	registry.RegisterProvider(genericProvider)
	registry.RegisterProvider(gitlabProvider)
	registry.RegisterProvider(githubProvider)

	tests := []struct {
		name        string
		issuerURL   string
		expected    OIDCProvider
		description string
	}{
		{
			name:        "GitLab.com official",
			issuerURL:   "https://gitlab.com",
			expected:    gitlabProvider,
			description: "Should detect official GitLab.com",
		},
		{
			name:        "GitLab.com with path",
			issuerURL:   "https://gitlab.com/oauth/authorize",
			expected:    gitlabProvider,
			description: "Should detect GitLab.com with path",
		},
		{
			name:        "Self-hosted gitlab.example.com",
			issuerURL:   "https://gitlab.example.com",
			expected:    gitlabProvider,
			description: "Should detect gitlab as subdomain",
		},
		{
			name:        "Self-hosted my.gitlab.io",
			issuerURL:   "https://my.gitlab.io",
			expected:    gitlabProvider,
			description: "Should detect gitlab in domain",
		},
		{
			name:        "Self-hosted example-gitlab.com",
			issuerURL:   "https://example-gitlab.com",
			expected:    gitlabProvider,
			description: "Should detect gitlab as suffix",
		},
		{
			name:        "Self-hosted gitlab-prod.company.net",
			issuerURL:   "https://gitlab-prod.company.net",
			expected:    gitlabProvider,
			description: "Should detect gitlab as prefix",
		},
		{
			name:        "Self-hosted my-gitlab.internal",
			issuerURL:   "https://my-gitlab.internal",
			expected:    gitlabProvider,
			description: "Should detect gitlab in middle of host",
		},
		{
			name:        "Self-hosted company.gitlab.services",
			issuerURL:   "https://company.gitlab.services",
			expected:    gitlabProvider,
			description: "Should detect gitlab in middle of domain",
		},
		{
			name:        "Self-hosted with port",
			issuerURL:   "https://gitlab.example.com:8443",
			expected:    gitlabProvider,
			description: "Should detect GitLab with custom port",
		},
		{
			name:        "Self-hosted with path and query",
			issuerURL:   "https://gitlab.example.com/oauth?param=value",
			expected:    gitlabProvider,
			description: "Should detect GitLab with complex URL",
		},
		{
			name:        "Case insensitive - GITLAB",
			issuerURL:   "https://GITLAB.example.com",
			expected:    gitlabProvider,
			description: "Should detect GitLab case-insensitively",
		},
		{
			name:        "Case insensitive - GitLab",
			issuerURL:   "https://GitLab.example.com",
			expected:    gitlabProvider,
			description: "Should detect GitLab with mixed case",
		},
		{
			name:        "Not GitLab - git prefix only",
			issuerURL:   "https://github.com",
			expected:    githubProvider, // Should match GitHub provider, not GitLab
			description: "Should not match github.com as GitLab",
		},
		{
			name:        "Not GitLab - lab suffix only",
			issuerURL:   "https://mylab.example.com",
			expected:    genericProvider,
			description: "Should not match partial gitlab string",
		},
		{
			name:        "Not GitLab - git and lab separate",
			issuerURL:   "https://git.mylab.example.com",
			expected:    genericProvider,
			description: "Should not match git and lab when not together",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Clear cache to ensure fresh detection
			registry.ClearCache()

			result := registry.DetectProvider(tt.issuerURL)

			if result != tt.expected {
				t.Errorf("%s: Expected %v, got %v", tt.description, tt.expected, result)
			}
		})
	}
}

// TestProviderRegistry_GitLabDetection_RealWorldURLs tests real-world GitLab URLs
func TestProviderRegistry_GitLabDetection_RealWorldURLs(t *testing.T) {
	registry := NewProviderRegistry()

	genericProvider := NewGenericProvider()
	gitlabProvider := NewGitLabProvider()
	githubProvider := NewGitHubProvider()

	registry.RegisterProvider(genericProvider)
	registry.RegisterProvider(gitlabProvider)
	registry.RegisterProvider(githubProvider)

	realWorldTests := []struct {
		expected  OIDCProvider
		name      string
		issuerURL string
	}{
		// Actual self-hosted GitLab examples from issue #61
		{
			name:      "Company self-hosted GitLab",
			issuerURL: "https://gitlab.company.com",
			expected:  gitlabProvider,
		},
		{
			name:      "Organization GitLab instance with gitlab in subdomain",
			issuerURL: "https://gitlab.organization.org",
			expected:  gitlabProvider,
		},
		{
			name:      "Internal GitLab server",
			issuerURL: "https://gitlab.internal.corp",
			expected:  gitlabProvider,
		},
		{
			name:      "GitLab with custom subdomain",
			issuerURL: "https://code.gitlab.mycompany.com",
			expected:  gitlabProvider,
		},
		// Negative cases to ensure we don't over-match
		{
			name:      "GitHub should not match GitLab",
			issuerURL: "https://github.com",
			expected:  githubProvider,
		},
		{
			name:      "Generic git server",
			issuerURL: "https://git.example.com",
			expected:  genericProvider,
		},
	}

	for _, tt := range realWorldTests {
		t.Run(tt.name, func(t *testing.T) {
			registry.ClearCache()
			result := registry.DetectProvider(tt.issuerURL)

			if result != tt.expected {
				var expectedType, resultType string
				if tt.expected != nil {
					expectedType = fmt.Sprintf("%v", tt.expected.GetType())
				} else {
					expectedType = "nil"
				}
				if result != nil {
					resultType = fmt.Sprintf("%v", result.GetType())
				} else {
					resultType = "nil"
				}

				t.Errorf("Expected provider type %s, got %s for URL %s",
					expectedType, resultType, tt.issuerURL)
			}
		})
	}
}

// Benchmark tests
func BenchmarkProviderRegistry_DetectProvider_Cached(b *testing.B) {
	registry := NewProviderRegistry()
	genericProvider := NewGenericProvider()
	registry.RegisterProvider(genericProvider)

	issuerURL := "https://auth.example.com"
	// Warm up cache
	registry.DetectProvider(issuerURL)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		registry.DetectProvider(issuerURL)
	}
}

func BenchmarkProviderRegistry_DetectProvider_Uncached(b *testing.B) {
	registry := NewProviderRegistry()
	genericProvider := NewGenericProvider()
	registry.RegisterProvider(genericProvider)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		registry.ClearCache() // Clear cache for each iteration
		registry.DetectProvider("https://auth.example.com")
	}
}

func BenchmarkProviderRegistry_RegisterProvider(b *testing.B) {
	registry := NewProviderRegistry()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		provider := NewGenericProvider()
		registry.RegisterProvider(provider)
	}
}
