package auth

import (
	"context"
	"testing"
	"time"
)

func TestNewSSOManager(t *testing.T) {
	m := NewSSOManager()
	if m == nil {
		t.Fatal("NewSSOManager() returned nil")
	}
	if m.sessions == nil {
		t.Error("NewSSOManager() did not initialize sessions map")
	}
}

func TestGenerateSecureToken(t *testing.T) {
	token, err := generateSecureToken(32)
	if err != nil {
		t.Fatalf("generateSecureToken() error = %v", err)
	}
	if len(token) != 32 {
		t.Errorf("generateSecureToken() length = %d, want 32", len(token))
	}

	// Generate another token and verify they're different
	token2, err := generateSecureToken(32)
	if err != nil {
		t.Fatalf("generateSecureToken() error = %v", err)
	}
	if token == token2 {
		t.Error("generateSecureToken() generated identical tokens")
	}
}

func TestSSOManager_GenerateAuthorizationCode(t *testing.T) {
	m := NewSSOManager()

	code, err := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 123, "testuser")
	if err != nil {
		t.Fatalf("GenerateAuthorizationCode() error = %v", err)
	}
	if code == "" {
		t.Error("GenerateAuthorizationCode() returned empty code")
	}

	// Verify session was stored
	m.mu.RLock()
	_, exists := m.sessions[code]
	m.mu.RUnlock()

	if !exists {
		t.Error("GenerateAuthorizationCode() did not store session")
	}
}

func TestSSOManager_ValidateAuthorizationCode(t *testing.T) {
	m := NewSSOManager()

	// Generate a code first
	code, _ := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 123, "testuser")

	session, err := m.ValidateAuthorizationCode(code)
	if err != nil {
		t.Fatalf("ValidateAuthorizationCode() error = %v", err)
	}

	if session.UserID != 123 {
		t.Errorf("UserID = %d, want 123", session.UserID)
	}
	if session.Username != "testuser" {
		t.Errorf("Username = %s, want testuser", session.Username)
	}
	if session.ClientID != "client-1" {
		t.Errorf("ClientID = %s, want client-1", session.ClientID)
	}

	// Code should be consumed (one-time use)
	_, err = m.ValidateAuthorizationCode(code)
	if err == nil {
		t.Error("ValidateAuthorizationCode() should return error for consumed code")
	}
}

func TestSSOManager_ValidateAuthorizationCode_Invalid(t *testing.T) {
	m := NewSSOManager()

	_, err := m.ValidateAuthorizationCode("invalid-code")
	if err == nil {
		t.Error("ValidateAuthorizationCode() should return error for invalid code")
	}
}

func TestSSOManager_ValidateAuthorizationCode_Expired(t *testing.T) {
	m := NewSSOManager()

	// Generate a code
	code, _ := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 123, "testuser")

	// Manually expire it
	m.mu.Lock()
	session := m.sessions[code]
	session.ExpiresAt = time.Now().Add(-1 * time.Hour)
	m.mu.Unlock()

	_, err := m.ValidateAuthorizationCode(code)
	if err == nil {
		t.Error("ValidateAuthorizationCode() should return error for expired code")
	}
}

func TestSSOManager_GenerateAccessToken(t *testing.T) {
	m := NewSSOManager()

	session := &SSOSession{
		UserID:   123,
		Username: "testuser",
		Scope:    "openid",
	}

	token, expiresAt, err := m.GenerateAccessToken("client-1", session)
	if err != nil {
		t.Fatalf("GenerateAccessToken() error = %v", err)
	}
	if token == "" {
		t.Error("GenerateAccessToken() returned empty token")
	}
	if expiresAt.Before(time.Now()) {
		t.Error("GenerateAccessToken() returned expired time")
	}

	// Verify token was stored
	m.mu.RLock()
	storedSession, exists := m.sessions[token]
	m.mu.RUnlock()

	if !exists {
		t.Error("GenerateAccessToken() did not store session")
	}
	if storedSession.UserID != 123 {
		t.Errorf("Stored UserID = %d, want 123", storedSession.UserID)
	}
}

func TestSSOManager_IntrospectToken(t *testing.T) {
	m := NewSSOManager()

	session := &SSOSession{
		UserID:   123,
		Username: "testuser",
		Scope:    "openid",
	}
	token, _, _ := m.GenerateAccessToken("client-1", session)

	info, err := m.IntrospectToken(token)
	if err != nil {
		t.Fatalf("IntrospectToken() error = %v", err)
	}

	if !info.Active {
		t.Error("IntrospectToken() returned inactive for valid token")
	}
	if info.UserID != 123 {
		t.Errorf("UserID = %d, want 123", info.UserID)
	}
	if info.Username != "testuser" {
		t.Errorf("Username = %s, want testuser", info.Username)
	}
}

func TestSSOManager_IntrospectToken_Invalid(t *testing.T) {
	m := NewSSOManager()

	info, err := m.IntrospectToken("invalid-token")
	if err != nil {
		t.Fatalf("IntrospectToken() error = %v", err)
	}

	if info.Active {
		t.Error("IntrospectToken() should return inactive for invalid token")
	}
}

func TestSSOManager_IntrospectToken_Expired(t *testing.T) {
	m := NewSSOManager()

	session := &SSOSession{
		UserID:   123,
		Username: "testuser",
		Scope:    "openid",
	}
	token, _, _ := m.GenerateAccessToken("client-1", session)

	// Manually expire it
	m.mu.Lock()
	m.sessions[token].ExpiresAt = time.Now().Add(-1 * time.Hour)
	m.mu.Unlock()

	info, err := m.IntrospectToken(token)
	if err != nil {
		t.Fatalf("IntrospectToken() error = %v", err)
	}

	if info.Active {
		t.Error("IntrospectToken() should return inactive for expired token")
	}
}

func TestSSOManager_RevokeToken(t *testing.T) {
	m := NewSSOManager()

	session := &SSOSession{
		UserID:   123,
		Username: "testuser",
		Scope:    "openid",
	}
	token, _, _ := m.GenerateAccessToken("client-1", session)

	err := m.RevokeToken(token)
	if err != nil {
		t.Fatalf("RevokeToken() error = %v", err)
	}

	// Token should be removed
	m.mu.RLock()
	_, exists := m.sessions[token]
	m.mu.RUnlock()

	if exists {
		t.Error("RevokeToken() did not remove token")
	}
}

func TestSSOManager_CleanupExpired(t *testing.T) {
	m := NewSSOManager()

	// Add sessions
	session1 := &SSOSession{
		UserID:    123,
		Username:  "user1",
		Scope:     "openid",
		CreatedAt: time.Now(),
		ExpiresAt: time.Now().Add(1 * time.Hour), // Valid
	}
	session2 := &SSOSession{
		UserID:    456,
		Username:  "user2",
		Scope:     "openid",
		CreatedAt: time.Now(),
		ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired
	}

	m.mu.Lock()
	m.sessions["valid-token"] = session1
	m.sessions["expired-token"] = session2
	m.mu.Unlock()

	m.CleanupExpired()

	m.mu.RLock()
	defer m.mu.RUnlock()

	// Valid session should remain
	if _, exists := m.sessions["valid-token"]; !exists {
		t.Error("CleanupExpired() removed valid session")
	}

	// Expired session should be removed
	if _, exists := m.sessions["expired-token"]; exists {
		t.Error("CleanupExpired() did not remove expired session")
	}
}

func TestSSOManager_evictOldest(t *testing.T) {
	m := NewSSOManager()

	// Add sessions with different creation times
	oldSession := &SSOSession{
		UserID:    123,
		Username:  "old-user",
		Scope:     "openid",
		CreatedAt: time.Now().Add(-1 * time.Hour),
		ExpiresAt: time.Now().Add(1 * time.Hour),
	}
	newSession := &SSOSession{
		UserID:    456,
		Username:  "new-user",
		Scope:     "openid",
		CreatedAt: time.Now(),
		ExpiresAt: time.Now().Add(1 * time.Hour),
	}

	m.mu.Lock()
	m.sessions["old-token"] = oldSession
	m.sessions["new-token"] = newSession
	m.mu.Unlock()

	m.mu.Lock()
	m.evictOldest()
	m.mu.Unlock()

	// Oldest session should be removed
	m.mu.RLock()
	defer m.mu.RUnlock()

	if _, exists := m.sessions["old-token"]; exists {
		t.Error("evictOldest() did not remove oldest session")
	}
	if _, exists := m.sessions["new-token"]; !exists {
		t.Error("evictOldest() removed newer session")
	}
}

func TestSSOManager_evictOldest_Empty(t *testing.T) {
	m := NewSSOManager()

	// Should not panic with empty sessions
	m.mu.Lock()
	m.evictOldest()
	m.mu.Unlock()
}

func TestSSOManager_SessionCount(t *testing.T) {
	m := NewSSOManager()

	if m.SessionCount() != 0 {
		t.Errorf("SessionCount() = %d, want 0", m.SessionCount())
	}

	m.mu.Lock()
	m.sessions["token1"] = &SSOSession{UserID: 1}
	m.sessions["token2"] = &SSOSession{UserID: 2}
	m.mu.Unlock()

	if m.SessionCount() != 2 {
		t.Errorf("SessionCount() = %d, want 2", m.SessionCount())
	}
}

func TestSSOManager_StartCleanup(t *testing.T) {
	m := NewSSOManager()

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

	m.StartCleanup(ctx)

	// Add an expired session
	m.mu.Lock()
	m.sessions["expired"] = &SSOSession{
		UserID:    1,
		ExpiresAt: time.Now().Add(-1 * time.Hour),
	}
	m.mu.Unlock()

	// Let cleanup run
	time.Sleep(100 * time.Millisecond)

	// Cancel context to stop cleanup
	cancel()
	time.Sleep(100 * time.Millisecond)
}

func TestSSOManager_MaxSessions(t *testing.T) {
	m := NewSSOManager()

	// Fill up sessions to max
	for i := 0; i < MaxSessions; i++ {
		token, _ := generateSecureToken(32)
		m.mu.Lock()
		m.sessions[token] = &SSOSession{
			UserID:    int64(i),
			CreatedAt: time.Now().Add(-time.Duration(i) * time.Second),
			ExpiresAt: time.Now().Add(1 * time.Hour),
		}
		m.mu.Unlock()
	}

	// Generate one more - should trigger eviction
	code, err := m.GenerateAuthorizationCode("client-1", "https://example.com/callback", "openid", 99999, "newuser")
	if err != nil {
		t.Fatalf("GenerateAuthorizationCode() error = %v", err)
	}

	// New session should exist
	m.mu.RLock()
	_, exists := m.sessions[code]
	m.mu.RUnlock()

	if !exists {
		t.Error("GenerateAuthorizationCode() did not store session at max capacity")
	}
}

func TestSSOManager_GenerateAccessToken_MaxSessions(t *testing.T) {
	m := NewSSOManager()

	// Fill up sessions to max
	for i := 0; i < MaxSessions; i++ {
		token, _ := generateSecureToken(32)
		m.mu.Lock()
		m.sessions[token] = &SSOSession{
			UserID:    int64(i),
			CreatedAt: time.Now().Add(-time.Duration(i) * time.Second),
			ExpiresAt: time.Now().Add(1 * time.Hour),
		}
		m.mu.Unlock()
	}

	// Generate access token - should trigger eviction
	session := &SSOSession{
		UserID:   99999,
		Username: "newuser",
		Scope:    "openid",
	}

	token, expiresAt, err := m.GenerateAccessToken("client-1", session)
	if err != nil {
		t.Fatalf("GenerateAccessToken() error = %v", err)
	}
	if token == "" {
		t.Error("GenerateAccessToken() returned empty token")
	}
	if expiresAt.Before(time.Now()) {
		t.Error("GenerateAccessToken() returned expired time")
	}

	// Verify token was stored
	m.mu.RLock()
	_, exists := m.sessions[token]
	m.mu.RUnlock()

	if !exists {
		t.Error("GenerateAccessToken() did not store session at max capacity")
	}
}

func TestSSOManager_GenerateAccessToken_WithExpiredSessions(t *testing.T) {
	m := NewSSOManager()

	// Add some expired sessions
	for i := 0; i < 5; i++ {
		token, _ := generateSecureToken(32)
		m.mu.Lock()
		m.sessions[token] = &SSOSession{
			UserID:    int64(i),
			CreatedAt: time.Now().Add(-2 * time.Hour),
			ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired
		}
		m.mu.Unlock()
	}

	// Generate access token - should clean up expired sessions first
	session := &SSOSession{
		UserID:   123,
		Username: "testuser",
		Scope:    "openid",
	}

	_, _, err := m.GenerateAccessToken("client-1", session)
	if err != nil {
		t.Fatalf("GenerateAccessToken() error = %v", err)
	}

	// Verify expired sessions were cleaned
	m.mu.RLock()
	count := len(m.sessions)
	m.mu.RUnlock()

	if count > MaxSessions {
		t.Errorf("Session count %d exceeds max %d", count, MaxSessions)
	}
}

// DefaultSSOClientsStore tests

func TestNewDefaultSSOClientsStore(t *testing.T) {
	store := NewDefaultSSOClientsStore()
	if store == nil {
		t.Fatal("NewDefaultSSOClientsStore() returned nil")
	}
	if store.clients == nil {
		t.Error("NewDefaultSSOClientsStore() did not initialize clients map")
	}
}

func TestDefaultSSOClientsStore_RegisterClient(t *testing.T) {
	store := NewDefaultSSOClientsStore()

	client := &SSOClient{
		ClientID:     "client-1",
		ClientSecret: "secret",
		Name:         "Test Client",
		RedirectURIs: []string{"https://example.com/callback"},
	}

	store.RegisterClient(client)

	retrieved, err := store.GetByClientID("client-1")
	if err != nil {
		t.Fatalf("GetByClientID() error = %v", err)
	}
	if retrieved.Name != "Test Client" {
		t.Errorf("Name = %s, want Test Client", retrieved.Name)
	}
}

func TestDefaultSSOClientsStore_GetByClientID_NotFound(t *testing.T) {
	store := NewDefaultSSOClientsStore()

	_, err := store.GetByClientID("non-existent")
	if err == nil {
		t.Error("GetByClientID() should return error for non-existent client")
	}
}

func TestDefaultSSOClientsStore_ValidateClientRedirectURI(t *testing.T) {
	store := NewDefaultSSOClientsStore()

	client := &SSOClient{
		ClientID:     "client-1",
		ClientSecret: "secret",
		Name:         "Test Client",
		RedirectURIs: []string{"https://example.com/callback", "https://app.com/auth"},
	}
	store.RegisterClient(client)

	tests := []struct {
		name        string
		clientID    string
		redirectURI string
		want        bool
	}{
		{"valid URI", "client-1", "https://example.com/callback", true},
		{"another valid URI", "client-1", "https://app.com/auth", true},
		{"invalid URI", "client-1", "https://evil.com/callback", false},
		{"invalid client", "unknown", "https://example.com/callback", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := store.ValidateClientRedirectURI(tt.clientID, tt.redirectURI)
			if result != tt.want {
				t.Errorf("ValidateClientRedirectURI() = %v, want %v", result, tt.want)
			}
		})
	}
}
