package providers

import (
	"errors"
	"testing"
	"time"
)

// Mock implementations for testing
type mockSession struct {
	idToken       string
	accessToken   string
	refreshToken  string
	authenticated bool
}

func (s *mockSession) GetIDToken() string      { return s.idToken }
func (s *mockSession) GetAccessToken() string  { return s.accessToken }
func (s *mockSession) GetRefreshToken() string { return s.refreshToken }
func (s *mockSession) GetAuthenticated() bool  { return s.authenticated }

type mockTokenVerifier struct {
	error error
}

func (v *mockTokenVerifier) VerifyToken(token string) error {
	return v.error
}

type mockTokenCache struct {
	claims map[string]map[string]interface{}
}

func (c *mockTokenCache) Get(key string) (map[string]interface{}, bool) {
	claims, exists := c.claims[key]
	return claims, exists
}

// TestBaseProvider_GetType tests the default provider type
func TestBaseProvider_GetType(t *testing.T) {
	provider := NewBaseProvider()

	if provider.GetType() != ProviderTypeGeneric {
		t.Errorf("Expected ProviderTypeGeneric, got %v", provider.GetType())
	}
}

// TestBaseProvider_GetCapabilities tests the default capabilities
func TestBaseProvider_GetCapabilities(t *testing.T) {
	provider := NewBaseProvider()
	capabilities := provider.GetCapabilities()

	if !capabilities.SupportsRefreshTokens {
		t.Error("Expected SupportsRefreshTokens to be true")
	}

	if !capabilities.RequiresOfflineAccessScope {
		t.Error("Expected RequiresOfflineAccessScope to be true")
	}

	if capabilities.PreferredTokenValidation != "id" {
		t.Errorf("Expected PreferredTokenValidation 'id', got '%s'", capabilities.PreferredTokenValidation)
	}

	if capabilities.RequiresPromptConsent {
		t.Error("Expected RequiresPromptConsent to be false")
	}
}

// TestBaseProvider_ValidateTokens_Unauthenticated tests validation when not authenticated
func TestBaseProvider_ValidateTokens_Unauthenticated(t *testing.T) {
	provider := NewBaseProvider()
	session := &mockSession{authenticated: false}
	verifier := &mockTokenVerifier{}
	cache := &mockTokenCache{}

	tests := []struct {
		name           string
		refreshToken   string
		expectedResult ValidationResult
	}{
		{
			name:         "No refresh token",
			refreshToken: "",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  false,
				IsExpired:     false,
			},
		},
		{
			name:         "Has refresh token",
			refreshToken: "refresh-token",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			session.refreshToken = tt.refreshToken

			result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)

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

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

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

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

// TestBaseProvider_ValidateTokens_AuthenticatedNoAccessToken tests authenticated session without access token
func TestBaseProvider_ValidateTokens_AuthenticatedNoAccessToken(t *testing.T) {
	provider := NewBaseProvider()
	session := &mockSession{
		authenticated: true,
		accessToken:   "", // No access token
	}
	verifier := &mockTokenVerifier{}
	cache := &mockTokenCache{}

	tests := []struct {
		name           string
		refreshToken   string
		expectedResult ValidationResult
	}{
		{
			name:         "No access token, no refresh token",
			refreshToken: "",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  false,
				IsExpired:     true,
			},
		},
		{
			name:         "No access token, has refresh token",
			refreshToken: "refresh-token",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			session.refreshToken = tt.refreshToken

			result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)

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

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

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

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

// TestBaseProvider_ValidateTokens_AuthenticatedNoIDToken tests authenticated session without ID token
func TestBaseProvider_ValidateTokens_AuthenticatedNoIDToken(t *testing.T) {
	provider := NewBaseProvider()
	session := &mockSession{
		authenticated: true,
		accessToken:   "access-token",
		idToken:       "", // No ID token
	}
	verifier := &mockTokenVerifier{}
	cache := &mockTokenCache{}

	tests := []struct {
		name           string
		refreshToken   string
		expectedResult ValidationResult
	}{
		{
			name:         "No ID token, no refresh token",
			refreshToken: "",
			expectedResult: ValidationResult{
				Authenticated: true,
				NeedsRefresh:  false,
				IsExpired:     false,
			},
		},
		{
			name:         "No ID token, has refresh token",
			refreshToken: "refresh-token",
			expectedResult: ValidationResult{
				Authenticated: true,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			session.refreshToken = tt.refreshToken

			result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)

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

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

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

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

// TestBaseProvider_ValidateTokens_TokenVerificationFailure tests token verification failures
func TestBaseProvider_ValidateTokens_TokenVerificationFailure(t *testing.T) {
	provider := NewBaseProvider()
	session := &mockSession{
		authenticated: true,
		accessToken:   "access-token",
		idToken:       "id-token",
	}
	cache := &mockTokenCache{}

	tests := []struct {
		name           string
		verifierError  error
		refreshToken   string
		expectedResult ValidationResult
	}{
		{
			name:          "Token expired, has refresh token",
			verifierError: errors.New("token has expired"),
			refreshToken:  "refresh-token",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
		{
			name:          "Token expired, no refresh token",
			verifierError: errors.New("token has expired"),
			refreshToken:  "",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  false,
				IsExpired:     true,
			},
		},
		{
			name:          "Other verification error, has refresh token",
			verifierError: errors.New("invalid signature"),
			refreshToken:  "refresh-token",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
		{
			name:          "Other verification error, no refresh token",
			verifierError: errors.New("invalid signature"),
			refreshToken:  "",
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  false,
				IsExpired:     true,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			verifier := &mockTokenVerifier{error: tt.verifierError}
			session.refreshToken = tt.refreshToken

			result, err := provider.ValidateTokens(session, verifier, cache, time.Minute)

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

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

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

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

// TestBaseProvider_ValidateTokenExpiry tests token expiry validation logic
func TestBaseProvider_ValidateTokenExpiry(t *testing.T) {
	provider := NewBaseProvider()
	session := &mockSession{refreshToken: "refresh-token"}

	now := time.Now()
	gracePeriod := 5 * time.Minute

	tests := []struct {
		claims         map[string]interface{}
		name           string
		expectedResult ValidationResult
		cacheFound     bool
	}{
		{
			name:       "Token not found in cache, has refresh token",
			claims:     nil,
			cacheFound: false,
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
		{
			name:       "Claims without exp, has refresh token",
			claims:     map[string]interface{}{"sub": "user123"},
			cacheFound: true,
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
		{
			name: "Token expired (beyond grace period), has refresh token",
			claims: map[string]interface{}{
				"exp": float64(now.Add(-10 * time.Minute).Unix()),
			},
			cacheFound: true,
			expectedResult: ValidationResult{
				Authenticated: true,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
		{
			name: "Token expires within grace period, has refresh token",
			claims: map[string]interface{}{
				"exp": float64(now.Add(2 * time.Minute).Unix()),
			},
			cacheFound: true,
			expectedResult: ValidationResult{
				Authenticated: true,
				NeedsRefresh:  true,
				IsExpired:     false,
			},
		},
		{
			name: "Token valid (beyond grace period)",
			claims: map[string]interface{}{
				"exp": float64(now.Add(10 * time.Minute).Unix()),
			},
			cacheFound: true,
			expectedResult: ValidationResult{
				Authenticated: true,
				NeedsRefresh:  false,
				IsExpired:     false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cache := &mockTokenCache{claims: make(map[string]map[string]interface{})}
			if tt.cacheFound {
				cache.claims["test-token"] = tt.claims
			}

			result, err := provider.ValidateTokenExpiry(session, "test-token", cache, gracePeriod)

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

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

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

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

// TestBaseProvider_ValidateTokenExpiry_NoRefreshToken tests expiry validation without refresh token
func TestBaseProvider_ValidateTokenExpiry_NoRefreshToken(t *testing.T) {
	provider := NewBaseProvider()
	session := &mockSession{refreshToken: ""} // No refresh token

	now := time.Now()
	gracePeriod := 5 * time.Minute

	tests := []struct {
		claims         map[string]interface{}
		name           string
		expectedResult ValidationResult
		cacheFound     bool
	}{
		{
			name:       "Token not found in cache, no refresh token",
			claims:     nil,
			cacheFound: false,
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  false,
				IsExpired:     true,
			},
		},
		{
			name:       "Claims without exp, no refresh token",
			claims:     map[string]interface{}{"sub": "user123"},
			cacheFound: true,
			expectedResult: ValidationResult{
				Authenticated: false,
				NeedsRefresh:  false,
				IsExpired:     true,
			},
		},
		{
			name: "Token expires within grace period, no refresh token",
			claims: map[string]interface{}{
				"exp": float64(now.Add(2 * time.Minute).Unix()),
			},
			cacheFound: true,
			expectedResult: ValidationResult{
				Authenticated: true,
				NeedsRefresh:  false,
				IsExpired:     false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cache := &mockTokenCache{claims: make(map[string]map[string]interface{})}
			if tt.cacheFound {
				cache.claims["test-token"] = tt.claims
			}

			result, err := provider.ValidateTokenExpiry(session, "test-token", cache, gracePeriod)

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

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

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

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

// TestBaseProvider_BuildAuthParams tests authorization parameter building
func TestBaseProvider_BuildAuthParams(t *testing.T) {
	provider := NewBaseProvider()

	tests := []struct {
		name           string
		scopes         []string
		expectedScopes []string
	}{
		{
			name:           "No existing offline_access scope",
			scopes:         []string{"openid", "profile", "email"},
			expectedScopes: []string{"openid", "profile", "email", "offline_access"},
		},
		{
			name:           "Existing offline_access scope",
			scopes:         []string{"openid", "profile", "offline_access", "email"},
			expectedScopes: []string{"openid", "profile", "offline_access", "email"},
		},
		{
			name:           "Empty scopes",
			scopes:         []string{},
			expectedScopes: []string{"offline_access"},
		},
		{
			name:           "Only offline_access",
			scopes:         []string{"offline_access"},
			expectedScopes: []string{"offline_access"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			baseParams := make(map[string][]string)
			baseParams["client_id"] = []string{"test-client"}

			result, err := provider.BuildAuthParams(baseParams, tt.scopes)

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

			if len(result.Scopes) != len(tt.expectedScopes) {
				t.Errorf("Expected %d scopes, got %d", len(tt.expectedScopes), len(result.Scopes))
			}

			// Check that all expected scopes are present
			for _, expectedScope := range tt.expectedScopes {
				found := false
				for _, actualScope := range result.Scopes {
					if actualScope == expectedScope {
						found = true
						break
					}
				}
				if !found {
					t.Errorf("Expected scope '%s' not found in result", expectedScope)
				}
			}

			// Verify base parameters are preserved
			if result.URLValues.Get("client_id") != "test-client" {
				t.Errorf("Expected client_id 'test-client', got '%s'", result.URLValues.Get("client_id"))
			}
		})
	}
}

// TestBaseProvider_HandleTokenRefresh tests token refresh handling
func TestBaseProvider_HandleTokenRefresh(t *testing.T) {
	provider := NewBaseProvider()
	tokenData := &TokenResult{
		IDToken:      "new-id-token",
		AccessToken:  "new-access-token",
		RefreshToken: "new-refresh-token",
	}

	// Base provider should do nothing and return no error
	err := provider.HandleTokenRefresh(tokenData)

	if err != nil {
		t.Errorf("Expected no error, got %v", err)
	}
}

// TestBaseProvider_ValidateConfig tests configuration validation
func TestBaseProvider_ValidateConfig(t *testing.T) {
	provider := NewBaseProvider()

	// Base provider should always return valid configuration
	err := provider.ValidateConfig()

	if err != nil {
		t.Errorf("Expected no error, got %v", err)
	}
}

// TestNewBaseProvider tests the constructor
func TestNewBaseProvider(t *testing.T) {
	provider := NewBaseProvider()

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

	// Verify it implements the OIDCProvider interface
	var _ OIDCProvider = provider
}

// Benchmark tests
func BenchmarkBaseProvider_ValidateTokens(b *testing.B) {
	provider := NewBaseProvider()
	session := &mockSession{
		authenticated: true,
		idToken:       "test-token",
		accessToken:   "access-token",
		refreshToken:  "refresh-token",
	}
	verifier := &mockTokenVerifier{}
	cache := &mockTokenCache{
		claims: map[string]map[string]interface{}{
			"test-token": {
				"exp": float64(time.Now().Add(10 * time.Minute).Unix()),
				"sub": "user123",
			},
		},
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		provider.ValidateTokens(session, verifier, cache, time.Minute)
	}
}

func BenchmarkBaseProvider_BuildAuthParams(b *testing.B) {
	provider := NewBaseProvider()
	baseParams := make(map[string][]string)
	baseParams["client_id"] = []string{"test-client"}
	scopes := []string{"openid", "profile", "email"}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		provider.BuildAuthParams(baseParams, scopes)
	}
}
