package sts

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestDistributedSTSService verifies that multiple STS instances with identical configurations
// behave consistently across distributed environments
func TestDistributedSTSService(t *testing.T) {
	ctx := context.Background()

	// Common configuration for all instances
	commonConfig := &STSConfig{
		TokenDuration:    FlexibleDuration{time.Hour},
		MaxSessionLength: FlexibleDuration{12 * time.Hour},
		Issuer:           "distributed-sts-test",
		SigningKey:       []byte("test-signing-key-32-characters-long"),

		Providers: []*ProviderConfig{
			{
				Name:    "keycloak-oidc",
				Type:    "oidc",
				Enabled: true,
				Config: map[string]interface{}{
					"issuer":   "http://keycloak:8080/realms/seaweedfs-test",
					"clientId": "seaweedfs-s3",
					"jwksUri":  "http://keycloak:8080/realms/seaweedfs-test/protocol/openid-connect/certs",
				},
			},

			{
				Name:    "disabled-ldap",
				Type:    "oidc", // Use OIDC as placeholder since LDAP isn't implemented
				Enabled: false,
				Config: map[string]interface{}{
					"issuer":   "ldap://company.com",
					"clientId": "ldap-client",
				},
			},
		},
	}

	// Create multiple STS instances simulating distributed deployment
	instance1 := NewSTSService()
	instance2 := NewSTSService()
	instance3 := NewSTSService()

	// Initialize all instances with identical configuration
	err := instance1.Initialize(commonConfig)
	require.NoError(t, err, "Instance 1 should initialize successfully")

	err = instance2.Initialize(commonConfig)
	require.NoError(t, err, "Instance 2 should initialize successfully")

	err = instance3.Initialize(commonConfig)
	require.NoError(t, err, "Instance 3 should initialize successfully")

	// Manually register mock providers for testing (not available in production)
	mockProviderConfig := map[string]interface{}{
		"issuer":   "http://localhost:9999",
		"clientId": "test-client",
	}
	mockProvider1, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
	require.NoError(t, err)
	mockProvider2, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
	require.NoError(t, err)
	mockProvider3, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
	require.NoError(t, err)

	instance1.RegisterProvider(mockProvider1)
	instance2.RegisterProvider(mockProvider2)
	instance3.RegisterProvider(mockProvider3)

	// Verify all instances have identical provider configurations
	t.Run("provider_consistency", func(t *testing.T) {
		// All instances should have same number of providers
		assert.Len(t, instance1.providers, 2, "Instance 1 should have 2 enabled providers")
		assert.Len(t, instance2.providers, 2, "Instance 2 should have 2 enabled providers")
		assert.Len(t, instance3.providers, 2, "Instance 3 should have 2 enabled providers")

		// All instances should have same provider names
		instance1Names := instance1.getProviderNames()
		instance2Names := instance2.getProviderNames()
		instance3Names := instance3.getProviderNames()

		assert.ElementsMatch(t, instance1Names, instance2Names, "Instance 1 and 2 should have same providers")
		assert.ElementsMatch(t, instance2Names, instance3Names, "Instance 2 and 3 should have same providers")

		// Verify specific providers exist on all instances
		expectedProviders := []string{"keycloak-oidc", "test-mock-provider"}
		assert.ElementsMatch(t, instance1Names, expectedProviders, "Instance 1 should have expected providers")
		assert.ElementsMatch(t, instance2Names, expectedProviders, "Instance 2 should have expected providers")
		assert.ElementsMatch(t, instance3Names, expectedProviders, "Instance 3 should have expected providers")

		// Verify disabled providers are not loaded
		assert.NotContains(t, instance1Names, "disabled-ldap", "Disabled providers should not be loaded")
		assert.NotContains(t, instance2Names, "disabled-ldap", "Disabled providers should not be loaded")
		assert.NotContains(t, instance3Names, "disabled-ldap", "Disabled providers should not be loaded")
	})

	// Test token generation consistency across instances
	t.Run("token_generation_consistency", func(t *testing.T) {
		sessionId := "test-session-123"
		expiresAt := time.Now().Add(time.Hour)

		// Generate tokens from different instances
		token1, err1 := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
		token2, err2 := instance2.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
		token3, err3 := instance3.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)

		require.NoError(t, err1, "Instance 1 token generation should succeed")
		require.NoError(t, err2, "Instance 2 token generation should succeed")
		require.NoError(t, err3, "Instance 3 token generation should succeed")

		// All tokens should be different (due to timestamp variations)
		// But they should all be valid JWTs with same signing key
		assert.NotEmpty(t, token1)
		assert.NotEmpty(t, token2)
		assert.NotEmpty(t, token3)
	})

	// Test token validation consistency - any instance should validate tokens from any other instance
	t.Run("cross_instance_token_validation", func(t *testing.T) {
		sessionId := "cross-validation-session"
		expiresAt := time.Now().Add(time.Hour)

		// Generate token on instance 1
		token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
		require.NoError(t, err)

		// Validate on all instances
		claims1, err1 := instance1.tokenGenerator.ValidateSessionToken(token)
		claims2, err2 := instance2.tokenGenerator.ValidateSessionToken(token)
		claims3, err3 := instance3.tokenGenerator.ValidateSessionToken(token)

		require.NoError(t, err1, "Instance 1 should validate token from instance 1")
		require.NoError(t, err2, "Instance 2 should validate token from instance 1")
		require.NoError(t, err3, "Instance 3 should validate token from instance 1")

		// All instances should extract same session ID
		assert.Equal(t, sessionId, claims1.SessionId)
		assert.Equal(t, sessionId, claims2.SessionId)
		assert.Equal(t, sessionId, claims3.SessionId)

		assert.Equal(t, claims1.SessionId, claims2.SessionId)
		assert.Equal(t, claims2.SessionId, claims3.SessionId)
	})

	// Test provider access consistency
	t.Run("provider_access_consistency", func(t *testing.T) {
		// All instances should be able to access the same providers
		provider1, exists1 := instance1.providers["test-mock-provider"]
		provider2, exists2 := instance2.providers["test-mock-provider"]
		provider3, exists3 := instance3.providers["test-mock-provider"]

		assert.True(t, exists1, "Instance 1 should have test-mock-provider")
		assert.True(t, exists2, "Instance 2 should have test-mock-provider")
		assert.True(t, exists3, "Instance 3 should have test-mock-provider")

		assert.Equal(t, provider1.Name(), provider2.Name())
		assert.Equal(t, provider2.Name(), provider3.Name())

		// Test authentication with the mock provider on all instances
		testToken := "valid_test_token"

		identity1, err1 := provider1.Authenticate(ctx, testToken)
		identity2, err2 := provider2.Authenticate(ctx, testToken)
		identity3, err3 := provider3.Authenticate(ctx, testToken)

		require.NoError(t, err1, "Instance 1 provider should authenticate successfully")
		require.NoError(t, err2, "Instance 2 provider should authenticate successfully")
		require.NoError(t, err3, "Instance 3 provider should authenticate successfully")

		// All instances should return identical identity information
		assert.Equal(t, identity1.UserID, identity2.UserID)
		assert.Equal(t, identity2.UserID, identity3.UserID)
		assert.Equal(t, identity1.Email, identity2.Email)
		assert.Equal(t, identity2.Email, identity3.Email)
		assert.Equal(t, identity1.Provider, identity2.Provider)
		assert.Equal(t, identity2.Provider, identity3.Provider)
	})
}

// TestSTSConfigurationValidation tests configuration validation for distributed deployments
func TestSTSConfigurationValidation(t *testing.T) {
	t.Run("consistent_signing_keys_required", func(t *testing.T) {
		// Different signing keys should result in incompatible token validation
		config1 := &STSConfig{
			TokenDuration:    FlexibleDuration{time.Hour},
			MaxSessionLength: FlexibleDuration{12 * time.Hour},
			Issuer:           "test-sts",
			SigningKey:       []byte("signing-key-1-32-characters-long"),
		}

		config2 := &STSConfig{
			TokenDuration:    FlexibleDuration{time.Hour},
			MaxSessionLength: FlexibleDuration{12 * time.Hour},
			Issuer:           "test-sts",
			SigningKey:       []byte("signing-key-2-32-characters-long"), // Different key!
		}

		instance1 := NewSTSService()
		instance2 := NewSTSService()

		err1 := instance1.Initialize(config1)
		err2 := instance2.Initialize(config2)

		require.NoError(t, err1)
		require.NoError(t, err2)

		// Generate token on instance 1
		sessionId := "test-session"
		expiresAt := time.Now().Add(time.Hour)
		token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
		require.NoError(t, err)

		// Instance 1 should validate its own token
		_, err = instance1.tokenGenerator.ValidateSessionToken(token)
		assert.NoError(t, err, "Instance 1 should validate its own token")

		// Instance 2 should reject token from instance 1 (different signing key)
		_, err = instance2.tokenGenerator.ValidateSessionToken(token)
		assert.Error(t, err, "Instance 2 should reject token with different signing key")
	})

	t.Run("consistent_issuer_required", func(t *testing.T) {
		// Different issuers should result in incompatible tokens
		commonSigningKey := []byte("shared-signing-key-32-characters-lo")

		config1 := &STSConfig{
			TokenDuration:    FlexibleDuration{time.Hour},
			MaxSessionLength: FlexibleDuration{12 * time.Hour},
			Issuer:           "sts-instance-1",
			SigningKey:       commonSigningKey,
		}

		config2 := &STSConfig{
			TokenDuration:    FlexibleDuration{time.Hour},
			MaxSessionLength: FlexibleDuration{12 * time.Hour},
			Issuer:           "sts-instance-2", // Different issuer!
			SigningKey:       commonSigningKey,
		}

		instance1 := NewSTSService()
		instance2 := NewSTSService()

		err1 := instance1.Initialize(config1)
		err2 := instance2.Initialize(config2)

		require.NoError(t, err1)
		require.NoError(t, err2)

		// Generate token on instance 1
		sessionId := "test-session"
		expiresAt := time.Now().Add(time.Hour)
		token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
		require.NoError(t, err)

		// Instance 2 should reject token due to issuer mismatch
		// (Even though signing key is the same, issuer validation will fail)
		_, err = instance2.tokenGenerator.ValidateSessionToken(token)
		assert.Error(t, err, "Instance 2 should reject token with different issuer")
	})
}

// TestProviderFactoryDistributed tests the provider factory in distributed scenarios
func TestProviderFactoryDistributed(t *testing.T) {
	factory := NewProviderFactory()

	// Simulate configuration that would be identical across all instances
	configs := []*ProviderConfig{
		{
			Name:    "production-keycloak",
			Type:    "oidc",
			Enabled: true,
			Config: map[string]interface{}{
				"issuer":       "https://keycloak.company.com/realms/seaweedfs",
				"clientId":     "seaweedfs-prod",
				"clientSecret": "super-secret-key",
				"jwksUri":      "https://keycloak.company.com/realms/seaweedfs/protocol/openid-connect/certs",
				"scopes":       []string{"openid", "profile", "email", "roles"},
			},
		},
		{
			Name:    "backup-oidc",
			Type:    "oidc",
			Enabled: false, // Disabled by default
			Config: map[string]interface{}{
				"issuer":   "https://backup-oidc.company.com",
				"clientId": "seaweedfs-backup",
			},
		},
	}

	// Create providers multiple times (simulating multiple instances)
	providers1, err1 := factory.LoadProvidersFromConfig(configs)
	providers2, err2 := factory.LoadProvidersFromConfig(configs)
	providers3, err3 := factory.LoadProvidersFromConfig(configs)

	require.NoError(t, err1, "First load should succeed")
	require.NoError(t, err2, "Second load should succeed")
	require.NoError(t, err3, "Third load should succeed")

	// All instances should have same provider counts
	assert.Len(t, providers1, 1, "First instance should have 1 enabled provider")
	assert.Len(t, providers2, 1, "Second instance should have 1 enabled provider")
	assert.Len(t, providers3, 1, "Third instance should have 1 enabled provider")

	// All instances should have same provider names
	names1 := make([]string, 0, len(providers1))
	names2 := make([]string, 0, len(providers2))
	names3 := make([]string, 0, len(providers3))

	for name := range providers1 {
		names1 = append(names1, name)
	}
	for name := range providers2 {
		names2 = append(names2, name)
	}
	for name := range providers3 {
		names3 = append(names3, name)
	}

	assert.ElementsMatch(t, names1, names2, "Instance 1 and 2 should have same provider names")
	assert.ElementsMatch(t, names2, names3, "Instance 2 and 3 should have same provider names")

	// Verify specific providers
	expectedProviders := []string{"production-keycloak"}
	assert.ElementsMatch(t, names1, expectedProviders, "Should have expected enabled providers")

	// Verify disabled providers are not included
	assert.NotContains(t, names1, "backup-oidc", "Disabled providers should not be loaded")
	assert.NotContains(t, names2, "backup-oidc", "Disabled providers should not be loaded")
	assert.NotContains(t, names3, "backup-oidc", "Disabled providers should not be loaded")
}
