package s3api

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/gorilla/mux"
	"github.com/seaweedfs/seaweedfs/weed/iam/integration"
	"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
	"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
	"github.com/seaweedfs/seaweedfs/weed/iam/policy"
	"github.com/seaweedfs/seaweedfs/weed/iam/sts"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// createTestJWTEndToEnd creates a test JWT token with the specified issuer, subject and signing key
func createTestJWTEndToEnd(t *testing.T, issuer, subject, signingKey string) string {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"iss": issuer,
		"sub": subject,
		"aud": "test-client-id",
		"exp": time.Now().Add(time.Hour).Unix(),
		"iat": time.Now().Unix(),
		// Add claims that trust policy validation expects
		"idp": "test-oidc", // Identity provider claim for trust policy matching
	})

	tokenString, err := token.SignedString([]byte(signingKey))
	require.NoError(t, err)
	return tokenString
}

// TestS3EndToEndWithJWT tests complete S3 operations with JWT authentication
func TestS3EndToEndWithJWT(t *testing.T) {
	// Set up complete IAM system with S3 integration
	s3Server, iamManager := setupCompleteS3IAMSystem(t)

	// Test scenarios
	tests := []struct {
		name            string
		roleArn         string
		sessionName     string
		setupRole       func(ctx context.Context, manager *integration.IAMManager)
		s3Operations    []S3Operation
		expectedResults []bool // true = allow, false = deny
	}{
		{
			name:        "S3 Read-Only Role Complete Workflow",
			roleArn:     "arn:aws:iam::role/S3ReadOnlyRole",
			sessionName: "readonly-test-session",
			setupRole:   setupS3ReadOnlyRole,
			s3Operations: []S3Operation{
				{Method: "PUT", Path: "/test-bucket", Body: nil, Operation: "CreateBucket"},
				{Method: "GET", Path: "/test-bucket", Body: nil, Operation: "ListBucket"},
				{Method: "PUT", Path: "/test-bucket/test-file.txt", Body: []byte("test content"), Operation: "PutObject"},
				{Method: "GET", Path: "/test-bucket/test-file.txt", Body: nil, Operation: "GetObject"},
				{Method: "HEAD", Path: "/test-bucket/test-file.txt", Body: nil, Operation: "HeadObject"},
				{Method: "DELETE", Path: "/test-bucket/test-file.txt", Body: nil, Operation: "DeleteObject"},
			},
			expectedResults: []bool{false, true, false, true, true, false}, // Only read operations allowed
		},
		{
			name:        "S3 Admin Role Complete Workflow",
			roleArn:     "arn:aws:iam::role/S3AdminRole",
			sessionName: "admin-test-session",
			setupRole:   setupS3AdminRole,
			s3Operations: []S3Operation{
				{Method: "PUT", Path: "/admin-bucket", Body: nil, Operation: "CreateBucket"},
				{Method: "PUT", Path: "/admin-bucket/admin-file.txt", Body: []byte("admin content"), Operation: "PutObject"},
				{Method: "GET", Path: "/admin-bucket/admin-file.txt", Body: nil, Operation: "GetObject"},
				{Method: "DELETE", Path: "/admin-bucket/admin-file.txt", Body: nil, Operation: "DeleteObject"},
				{Method: "DELETE", Path: "/admin-bucket", Body: nil, Operation: "DeleteBucket"},
			},
			expectedResults: []bool{true, true, true, true, true}, // All operations allowed
		},
		{
			name:        "S3 IP-Restricted Role",
			roleArn:     "arn:aws:iam::role/S3IPRestrictedRole",
			sessionName: "ip-restricted-session",
			setupRole:   setupS3IPRestrictedRole,
			s3Operations: []S3Operation{
				{Method: "GET", Path: "/restricted-bucket/file.txt", Body: nil, Operation: "GetObject", SourceIP: "192.168.1.100"}, // Allowed IP
				{Method: "GET", Path: "/restricted-bucket/file.txt", Body: nil, Operation: "GetObject", SourceIP: "8.8.8.8"},       // Blocked IP
			},
			expectedResults: []bool{true, false}, // Only office IP allowed
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()

			// Set up role
			tt.setupRole(ctx, iamManager)

			// Create a valid JWT token for testing
			validJWTToken := createTestJWTEndToEnd(t, "https://test-issuer.com", "test-user-123", "test-signing-key")

			// Assume role to get JWT token
			response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
				RoleArn:          tt.roleArn,
				WebIdentityToken: validJWTToken,
				RoleSessionName:  tt.sessionName,
			})
			require.NoError(t, err, "Failed to assume role %s", tt.roleArn)

			jwtToken := response.Credentials.SessionToken
			require.NotEmpty(t, jwtToken, "JWT token should not be empty")

			// Execute S3 operations
			for i, operation := range tt.s3Operations {
				t.Run(fmt.Sprintf("%s_%s", tt.name, operation.Operation), func(t *testing.T) {
					allowed := executeS3OperationWithJWT(t, s3Server, operation, jwtToken)
					expected := tt.expectedResults[i]

					if expected {
						assert.True(t, allowed, "Operation %s should be allowed", operation.Operation)
					} else {
						assert.False(t, allowed, "Operation %s should be denied", operation.Operation)
					}
				})
			}
		})
	}
}

// TestS3MultipartUploadWithJWT tests multipart upload with IAM
func TestS3MultipartUploadWithJWT(t *testing.T) {
	s3Server, iamManager := setupCompleteS3IAMSystem(t)
	ctx := context.Background()

	// Set up write role
	setupS3WriteRole(ctx, iamManager)

	// Create a valid JWT token for testing
	validJWTToken := createTestJWTEndToEnd(t, "https://test-issuer.com", "test-user-123", "test-signing-key")

	// Assume role
	response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
		RoleArn:          "arn:aws:iam::role/S3WriteRole",
		WebIdentityToken: validJWTToken,
		RoleSessionName:  "multipart-test-session",
	})
	require.NoError(t, err)

	jwtToken := response.Credentials.SessionToken

	// Test multipart upload workflow
	tests := []struct {
		name      string
		operation S3Operation
		expected  bool
	}{
		{
			name: "Initialize Multipart Upload",
			operation: S3Operation{
				Method:    "POST",
				Path:      "/multipart-bucket/large-file.txt?uploads",
				Body:      nil,
				Operation: "CreateMultipartUpload",
			},
			expected: true,
		},
		{
			name: "Upload Part",
			operation: S3Operation{
				Method:    "PUT",
				Path:      "/multipart-bucket/large-file.txt?partNumber=1&uploadId=test-upload-id",
				Body:      bytes.Repeat([]byte("data"), 1024), // 4KB part
				Operation: "UploadPart",
			},
			expected: true,
		},
		{
			name: "List Parts",
			operation: S3Operation{
				Method:    "GET",
				Path:      "/multipart-bucket/large-file.txt?uploadId=test-upload-id",
				Body:      nil,
				Operation: "ListParts",
			},
			expected: true,
		},
		{
			name: "Complete Multipart Upload",
			operation: S3Operation{
				Method:    "POST",
				Path:      "/multipart-bucket/large-file.txt?uploadId=test-upload-id",
				Body:      []byte("<CompleteMultipartUpload></CompleteMultipartUpload>"),
				Operation: "CompleteMultipartUpload",
			},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			allowed := executeS3OperationWithJWT(t, s3Server, tt.operation, jwtToken)
			if tt.expected {
				assert.True(t, allowed, "Multipart operation %s should be allowed", tt.operation.Operation)
			} else {
				assert.False(t, allowed, "Multipart operation %s should be denied", tt.operation.Operation)
			}
		})
	}
}

// TestS3ListObjectsV2PrefixCondition tests that ListObjectsV2 requests with a prefix
// query parameter correctly populate s3:prefix in the policy evaluation context and
// use bucket-level resource ARNs, so that policies with s3:prefix conditions work.
// This reproduces the bug reported in https://github.com/seaweedfs/seaweedfs/issues/8969
func TestS3ListObjectsV2PrefixCondition(t *testing.T) {
	// Set up IAM system
	iamManager := integration.NewIAMManager()
	config := &integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{Duration: time.Hour},
			MaxSessionLength: sts.FlexibleDuration{Duration: time.Hour * 12},
			Issuer:           "test-sts",
			SigningKey:       []byte("test-signing-key-32-characters-long"),
		},
		Policy: &policy.PolicyEngineConfig{
			DefaultEffect: "Deny",
			StoreType:     "memory",
		},
		Roles: &integration.RoleStoreConfig{
			StoreType: "memory",
		},
	}

	err := iamManager.Initialize(config, func() string { return "localhost:8888" })
	require.NoError(t, err)

	setupTestProviders(t, iamManager)

	s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
	require.NotNil(t, s3IAMIntegration)

	ctx := context.Background()

	// Create a role with a policy that allows ListBucket only with a specific s3:prefix condition.
	// This is the pattern used by Lakekeeper-vended STS credentials (issue #8969).
	prefixPolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Sid:    "AllowListUnderWarehouse",
				Effect: "Allow",
				Action: []string{"s3:ListBucket"},
				Resource: []string{
					"arn:aws:s3:::examples",
				},
				Condition: map[string]map[string]interface{}{
					"StringLike": {
						"s3:prefix": []string{"warehouse/*"},
					},
				},
			},
			{
				Sid:      "AllowSTSSessionValidation",
				Effect:   "Allow",
				Action:   []string{"sts:ValidateSession"},
				Resource: []string{"*"},
			},
		},
	}

	iamManager.CreatePolicy(ctx, "", "PrefixRestrictedPolicy", prefixPolicy)
	iamManager.CreateRole(ctx, "", "PrefixRestrictedRole", &integration.RoleDefinition{
		RoleName: "PrefixRestrictedRole",
		TrustPolicy: &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect:    "Allow",
					Principal: map[string]interface{}{"Federated": "test-oidc"},
					Action:    []string{"sts:AssumeRoleWithWebIdentity"},
				},
			},
		},
		AttachedPolicies: []string{"PrefixRestrictedPolicy"},
	})

	// Assume role to get a session token
	validJWTToken := createTestJWTEndToEnd(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
	response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
		RoleArn:          "arn:aws:iam::role/PrefixRestrictedRole",
		WebIdentityToken: validJWTToken,
		RoleSessionName:  "prefix-test-session",
	})
	require.NoError(t, err)

	sessionToken := response.Credentials.SessionToken
	require.NotEmpty(t, sessionToken)

	// Authenticate to get IAM identity
	authReq := httptest.NewRequest("GET", "/examples", http.NoBody)
	authReq.Header.Set("Authorization", "Bearer "+sessionToken)
	identity, errCode := s3IAMIntegration.AuthenticateJWT(ctx, authReq)
	require.Equal(t, s3err.ErrNone, errCode, "Authentication should succeed")

	tests := []struct {
		name     string
		url      string
		bucket   string
		objKey   string
		expected s3err.ErrorCode
	}{
		{
			name:     "ListObjectsV2 with matching prefix query param and empty objectKey",
			url:      "/examples?list-type=2&prefix=warehouse/data",
			bucket:   "examples",
			objKey:   "",
			expected: s3err.ErrNone,
		},
		{
			name:     "ListObjectsV2 with matching prefix propagated as objectKey",
			url:      "/examples?list-type=2&prefix=warehouse/data",
			bucket:   "examples",
			objKey:   "warehouse/data",
			expected: s3err.ErrNone,
		},
		{
			name:     "ListObjectsV1 with matching prefix query param",
			url:      "/examples?prefix=warehouse/files",
			bucket:   "examples",
			objKey:   "",
			expected: s3err.ErrNone,
		},
		{
			name:     "ListObjectsV2 with non-matching prefix should be denied",
			url:      "/examples?list-type=2&prefix=other/path",
			bucket:   "examples",
			objKey:   "",
			expected: s3err.ErrAccessDenied,
		},
		{
			name:     "ListObjectsV2 with no prefix should be denied",
			url:      "/examples?list-type=2",
			bucket:   "examples",
			objKey:   "",
			expected: s3err.ErrAccessDenied,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest("GET", tt.url, http.NoBody)
			result := s3IAMIntegration.AuthorizeAction(ctx, identity, Action("List"), tt.bucket, tt.objKey, req)
			assert.Equal(t, tt.expected, result, "unexpected authorization result for %s", tt.name)
		})
	}
}

// TestS3CreateBucketWithAttachedPolicy verifies a federated session whose
// attached policy grants s3:CreateBucket on a bucket ARN pattern can create
// matching buckets. CreateBucket is registered with ACTION_ADMIN, which used
// to resolve to s3:* and made the operation unmatchable by any narrower grant.
func TestS3CreateBucketWithAttachedPolicy(t *testing.T) {
	iamManager := integration.NewIAMManager()
	config := &integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{Duration: time.Hour},
			MaxSessionLength: sts.FlexibleDuration{Duration: time.Hour * 12},
			Issuer:           "test-sts",
			SigningKey:       []byte("test-signing-key-32-characters-long"),
		},
		Policy: &policy.PolicyEngineConfig{
			DefaultEffect: "Deny",
			StoreType:     "memory",
		},
		Roles: &integration.RoleStoreConfig{
			StoreType: "memory",
		},
	}

	err := iamManager.Initialize(config, func() string { return "localhost:8888" })
	require.NoError(t, err)

	setupTestProviders(t, iamManager)

	s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
	ctx := context.Background()

	bucketPolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Sid:      "AllowBucketManagement",
				Effect:   "Allow",
				Action:   []string{"s3:CreateBucket", "s3:DeleteBucket"},
				Resource: []string{"arn:aws:s3:::ml-*"},
			},
			{
				Sid:      "AllowSTSSessionValidation",
				Effect:   "Allow",
				Action:   []string{"sts:ValidateSession"},
				Resource: []string{"*"},
			},
		},
	}

	iamManager.CreatePolicy(ctx, "", "BucketManagementPolicy", bucketPolicy)
	iamManager.CreateRole(ctx, "", "BucketManagerRole", &integration.RoleDefinition{
		RoleName: "BucketManagerRole",
		TrustPolicy: &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect:    "Allow",
					Principal: map[string]interface{}{"Federated": "test-oidc"},
					Action:    []string{"sts:AssumeRoleWithWebIdentity"},
				},
			},
		},
		AttachedPolicies: []string{"BucketManagementPolicy"},
	})

	validJWTToken := createTestJWTEndToEnd(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
	response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
		RoleArn:          "arn:aws:iam::role/BucketManagerRole",
		WebIdentityToken: validJWTToken,
		RoleSessionName:  "bucket-create-session",
	})
	require.NoError(t, err)

	authReq := httptest.NewRequest("PUT", "/ml-models", http.NoBody)
	authReq.Header.Set("Authorization", "Bearer "+response.Credentials.SessionToken)
	identity, errCode := s3IAMIntegration.AuthenticateJWT(ctx, authReq)
	require.Equal(t, s3err.ErrNone, errCode)

	tests := []struct {
		name     string
		method   string
		url      string
		bucket   string
		action   Action
		expected s3err.ErrorCode
	}{
		{
			name:     "CreateBucket on matching bucket is allowed",
			method:   "PUT",
			url:      "/ml-models",
			bucket:   "ml-models",
			action:   Action(s3_constants.ACTION_ADMIN),
			expected: s3err.ErrNone,
		},
		{
			name:     "CreateBucket outside the resource pattern is denied",
			method:   "PUT",
			url:      "/other-bucket",
			bucket:   "other-bucket",
			action:   Action(s3_constants.ACTION_ADMIN),
			expected: s3err.ErrAccessDenied,
		},
		{
			name:     "PutBucketEncryption is not swept into CreateBucket",
			method:   "PUT",
			url:      "/ml-models?encryption",
			bucket:   "ml-models",
			action:   Action(s3_constants.ACTION_ADMIN),
			expected: s3err.ErrAccessDenied,
		},
		{
			name:     "DeleteBucket on matching bucket is allowed",
			method:   "DELETE",
			url:      "/ml-models",
			bucket:   "ml-models",
			action:   Action(s3_constants.ACTION_DELETE_BUCKET),
			expected: s3err.ErrNone,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(tt.method, tt.url, http.NoBody)
			result := s3IAMIntegration.AuthorizeAction(ctx, identity, tt.action, tt.bucket, "", req)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestS3CORSWithJWT tests CORS preflight requests with IAM
func TestS3CORSWithJWT(t *testing.T) {
	s3Server, iamManager := setupCompleteS3IAMSystem(t)
	ctx := context.Background()

	// Set up read role
	setupS3ReadOnlyRole(ctx, iamManager)

	// Test CORS preflight
	req := httptest.NewRequest("OPTIONS", "/test-bucket/test-file.txt", http.NoBody)
	req.Header.Set("Origin", "https://example.com")
	req.Header.Set("Access-Control-Request-Method", "GET")
	req.Header.Set("Access-Control-Request-Headers", "Authorization")

	recorder := httptest.NewRecorder()
	s3Server.ServeHTTP(recorder, req)

	// CORS preflight should succeed
	assert.True(t, recorder.Code < 400, "CORS preflight should succeed, got %d: %s", recorder.Code, recorder.Body.String())

	// Check CORS headers
	assert.Contains(t, recorder.Header().Get("Access-Control-Allow-Origin"), "example.com")
	assert.Contains(t, recorder.Header().Get("Access-Control-Allow-Methods"), "GET")
}

// TestS3PerformanceWithIAM tests performance impact of IAM integration
func TestS3PerformanceWithIAM(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping performance test in short mode")
	}

	s3Server, iamManager := setupCompleteS3IAMSystem(t)
	ctx := context.Background()

	// Set up performance role
	setupS3ReadOnlyRole(ctx, iamManager)

	// Create a valid JWT token for testing
	validJWTToken := createTestJWTEndToEnd(t, "https://test-issuer.com", "test-user-123", "test-signing-key")

	// Assume role
	response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
		RoleArn:          "arn:aws:iam::role/S3ReadOnlyRole",
		WebIdentityToken: validJWTToken,
		RoleSessionName:  "performance-test-session",
	})
	require.NoError(t, err)

	jwtToken := response.Credentials.SessionToken

	// Benchmark multiple GET requests
	numRequests := 100
	start := time.Now()

	for i := 0; i < numRequests; i++ {
		operation := S3Operation{
			Method:    "GET",
			Path:      fmt.Sprintf("/perf-bucket/file-%d.txt", i),
			Body:      nil,
			Operation: "GetObject",
		}

		executeS3OperationWithJWT(t, s3Server, operation, jwtToken)
	}

	duration := time.Since(start)
	avgLatency := duration / time.Duration(numRequests)

	t.Logf("Performance Results:")
	t.Logf("- Total requests: %d", numRequests)
	t.Logf("- Total time: %v", duration)
	t.Logf("- Average latency: %v", avgLatency)
	t.Logf("- Requests per second: %.2f", float64(numRequests)/duration.Seconds())

	// Assert reasonable performance (less than 10ms average)
	assert.Less(t, avgLatency, 10*time.Millisecond, "IAM overhead should be minimal")
}

// S3Operation represents an S3 operation for testing
type S3Operation struct {
	Method    string
	Path      string
	Body      []byte
	Operation string
	SourceIP  string
}

// Helper functions for test setup

func setupCompleteS3IAMSystem(t *testing.T) (http.Handler, *integration.IAMManager) {
	// Create IAM manager
	iamManager := integration.NewIAMManager()

	// Initialize with test configuration
	config := &integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{Duration: time.Hour},
			MaxSessionLength: sts.FlexibleDuration{Duration: time.Hour * 12},
			Issuer:           "test-sts",
			SigningKey:       []byte("test-signing-key-32-characters-long"),
		},
		Policy: &policy.PolicyEngineConfig{
			DefaultEffect: "Deny",
			StoreType:     "memory",
		},
		Roles: &integration.RoleStoreConfig{
			StoreType: "memory",
		},
	}

	err := iamManager.Initialize(config, func() string {
		return "localhost:8888" // Mock filer address for testing
	})
	require.NoError(t, err)

	// Set up test identity providers
	setupTestProviders(t, iamManager)

	// Create S3 server with IAM integration
	router := mux.NewRouter()

	// Create S3 IAM integration for testing with error recovery
	var s3IAMIntegration *S3IAMIntegration

	// Attempt to create IAM integration with panic recovery
	func() {
		defer func() {
			if r := recover(); r != nil {
				t.Logf("Failed to create S3 IAM integration: %v", r)
				t.Skip("Skipping test due to S3 server setup issues (likely missing filer or older code version)")
			}
		}()
		s3IAMIntegration = NewS3IAMIntegration(iamManager, "localhost:8888")
	}()

	if s3IAMIntegration == nil {
		t.Skip("Could not create S3 IAM integration")
	}

	// Add a simple test endpoint that we can use to verify IAM functionality
	router.HandleFunc("/test-auth", func(w http.ResponseWriter, r *http.Request) {
		// Test JWT authentication
		identity, errCode := s3IAMIntegration.AuthenticateJWT(r.Context(), r)
		if errCode != s3err.ErrNone {
			w.WriteHeader(http.StatusUnauthorized)
			w.Write([]byte("Authentication failed"))
			return
		}

		// Map HTTP method to S3 action for more realistic testing
		var action Action
		switch r.Method {
		case "GET":
			action = Action("s3:GetObject")
		case "PUT":
			action = Action("s3:PutObject")
		case "DELETE":
			action = Action("s3:DeleteObject")
		case "HEAD":
			action = Action("s3:HeadObject")
		default:
			action = Action("s3:GetObject") // Default fallback
		}

		// Test authorization with appropriate action
		authErrCode := s3IAMIntegration.AuthorizeAction(r.Context(), identity, action, "test-bucket", "test-object", r)
		if authErrCode != s3err.ErrNone {
			w.WriteHeader(http.StatusForbidden)
			w.Write([]byte("Authorization failed"))
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("Success"))
	}).Methods("GET", "PUT", "DELETE", "HEAD")

	// Add CORS preflight handler for S3 bucket/object paths
	router.PathPrefix("/{bucket}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "OPTIONS" {
			// Handle CORS preflight request
			origin := r.Header.Get("Origin")
			requestMethod := r.Header.Get("Access-Control-Request-Method")

			// Set CORS headers
			w.Header().Set("Access-Control-Allow-Origin", origin)
			w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, HEAD, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Amz-Date, X-Amz-Security-Token")
			w.Header().Set("Access-Control-Max-Age", "3600")

			if requestMethod != "" {
				w.Header().Add("Access-Control-Allow-Methods", requestMethod)
			}

			w.WriteHeader(http.StatusOK)
			return
		}

		// For non-OPTIONS requests, return 404 since we don't have full S3 implementation
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("Not found"))
	})

	return router, iamManager
}

func setupTestProviders(t *testing.T, manager *integration.IAMManager) {
	// Set up OIDC provider
	oidcProvider := oidc.NewMockOIDCProvider("test-oidc")
	oidcConfig := &oidc.OIDCConfig{
		Issuer:   "https://test-issuer.com",
		ClientID: "test-client-id",
	}
	err := oidcProvider.Initialize(oidcConfig)
	require.NoError(t, err)
	oidcProvider.SetupDefaultTestData()

	// Set up LDAP mock provider (no config needed for mock)
	ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
	err = ldapProvider.Initialize(nil) // Mock doesn't need real config
	require.NoError(t, err)
	ldapProvider.SetupDefaultTestData()

	// Register providers
	err = manager.RegisterIdentityProvider(oidcProvider)
	require.NoError(t, err)
	err = manager.RegisterIdentityProvider(ldapProvider)
	require.NoError(t, err)
}

func setupS3ReadOnlyRole(ctx context.Context, manager *integration.IAMManager) {
	// Create read-only policy
	readOnlyPolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Sid:    "AllowS3ReadOperations",
				Effect: "Allow",
				Action: []string{"s3:GetObject", "s3:ListBucket", "s3:HeadObject"},
				Resource: []string{
					"arn:aws:s3:::*",
					"arn:aws:s3:::*/*",
				},
			},
			{
				Sid:      "AllowSTSSessionValidation",
				Effect:   "Allow",
				Action:   []string{"sts:ValidateSession"},
				Resource: []string{"*"},
			},
		},
	}

	manager.CreatePolicy(ctx, "", "S3ReadOnlyPolicy", readOnlyPolicy)

	// Create role
	manager.CreateRole(ctx, "", "S3ReadOnlyRole", &integration.RoleDefinition{
		RoleName: "S3ReadOnlyRole",
		TrustPolicy: &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect: "Allow",
					Principal: map[string]interface{}{
						"Federated": "test-oidc",
					},
					Action: []string{"sts:AssumeRoleWithWebIdentity"},
				},
			},
		},
		AttachedPolicies: []string{"S3ReadOnlyPolicy"},
	})
}

func setupS3AdminRole(ctx context.Context, manager *integration.IAMManager) {
	// Create admin policy
	adminPolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Sid:    "AllowAllS3Operations",
				Effect: "Allow",
				Action: []string{"s3:*"},
				Resource: []string{
					"arn:aws:s3:::*",
					"arn:aws:s3:::*/*",
				},
			},
			{
				Sid:      "AllowSTSSessionValidation",
				Effect:   "Allow",
				Action:   []string{"sts:ValidateSession"},
				Resource: []string{"*"},
			},
		},
	}

	manager.CreatePolicy(ctx, "", "S3AdminPolicy", adminPolicy)

	// Create role
	manager.CreateRole(ctx, "", "S3AdminRole", &integration.RoleDefinition{
		RoleName: "S3AdminRole",
		TrustPolicy: &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect: "Allow",
					Principal: map[string]interface{}{
						"Federated": "test-oidc",
					},
					Action: []string{"sts:AssumeRoleWithWebIdentity"},
				},
			},
		},
		AttachedPolicies: []string{"S3AdminPolicy"},
	})
}

func setupS3WriteRole(ctx context.Context, manager *integration.IAMManager) {
	// Create write policy
	writePolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Sid:    "AllowS3WriteOperations",
				Effect: "Allow",
				Action: []string{"s3:PutObject", "s3:GetObject", "s3:ListBucket", "s3:DeleteObject"},
				Resource: []string{
					"arn:aws:s3:::*",
					"arn:aws:s3:::*/*",
				},
			},
			{
				Sid:      "AllowSTSSessionValidation",
				Effect:   "Allow",
				Action:   []string{"sts:ValidateSession"},
				Resource: []string{"*"},
			},
		},
	}

	manager.CreatePolicy(ctx, "", "S3WritePolicy", writePolicy)

	// Create role
	manager.CreateRole(ctx, "", "S3WriteRole", &integration.RoleDefinition{
		RoleName: "S3WriteRole",
		TrustPolicy: &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect: "Allow",
					Principal: map[string]interface{}{
						"Federated": "test-oidc",
					},
					Action: []string{"sts:AssumeRoleWithWebIdentity"},
				},
			},
		},
		AttachedPolicies: []string{"S3WritePolicy"},
	})
}

func setupS3IPRestrictedRole(ctx context.Context, manager *integration.IAMManager) {
	// Create IP-restricted policy
	restrictedPolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Sid:    "AllowS3FromOfficeIP",
				Effect: "Allow",
				Action: []string{"s3:GetObject", "s3:ListBucket"},
				Resource: []string{
					"arn:aws:s3:::*",
					"arn:aws:s3:::*/*",
				},
				Condition: map[string]map[string]interface{}{
					"IpAddress": {
						"aws:SourceIp": []string{"192.168.1.0/24"},
					},
				},
			},
			{
				Sid:      "AllowSTSSessionValidation",
				Effect:   "Allow",
				Action:   []string{"sts:ValidateSession"},
				Resource: []string{"*"},
			},
		},
	}

	manager.CreatePolicy(ctx, "", "S3IPRestrictedPolicy", restrictedPolicy)

	// Create role
	manager.CreateRole(ctx, "", "S3IPRestrictedRole", &integration.RoleDefinition{
		RoleName: "S3IPRestrictedRole",
		TrustPolicy: &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect: "Allow",
					Principal: map[string]interface{}{
						"Federated": "test-oidc",
					},
					Action: []string{"sts:AssumeRoleWithWebIdentity"},
				},
			},
		},
		AttachedPolicies: []string{"S3IPRestrictedPolicy"},
	})
}

func executeS3OperationWithJWT(t *testing.T, s3Server http.Handler, operation S3Operation, jwtToken string) bool {
	// Use our simplified test endpoint for IAM validation with the correct HTTP method
	req := httptest.NewRequest(operation.Method, "/test-auth", nil)
	req.Header.Set("Authorization", "Bearer "+jwtToken)
	req.Header.Set("Content-Type", "application/octet-stream")

	// Set source IP if specified
	if operation.SourceIP != "" {
		req.Header.Set("X-Forwarded-For", operation.SourceIP)
		req.RemoteAddr = operation.SourceIP + ":12345"
	}

	// Execute request
	recorder := httptest.NewRecorder()
	s3Server.ServeHTTP(recorder, req)

	// Determine if operation was allowed
	allowed := recorder.Code < 400

	t.Logf("S3 Operation: %s %s -> %d (%s)", operation.Method, operation.Path, recorder.Code,
		map[bool]string{true: "ALLOWED", false: "DENIED"}[allowed])

	if !allowed && recorder.Code != http.StatusForbidden && recorder.Code != http.StatusUnauthorized {
		// If it's not a 403/401, it might be a different error (like not found)
		// For testing purposes, we'll consider non-auth errors as "allowed" for now
		t.Logf("Non-auth error: %s", recorder.Body.String())
		return true
	}

	return allowed
}

// TestS3AuthenticationDenied tests that unauthenticated and invalid requests are properly rejected
func TestS3AuthenticationDenied(t *testing.T) {
	s3Server, _ := setupCompleteS3IAMSystem(t)

	tests := []struct {
		name           string
		setupRequest   func() *http.Request
		expectedStatus int
		description    string
	}{
		{
			name: "no_authorization_header",
			setupRequest: func() *http.Request {
				req := httptest.NewRequest("GET", "/test-auth", nil)
				// No Authorization header
				return req
			},
			expectedStatus: http.StatusUnauthorized,
			description:    "Request without Authorization header should be rejected",
		},
		{
			name: "empty_bearer_token",
			setupRequest: func() *http.Request {
				req := httptest.NewRequest("GET", "/test-auth", nil)
				req.Header.Set("Authorization", "Bearer ")
				return req
			},
			expectedStatus: http.StatusUnauthorized,
			description:    "Request with empty Bearer token should be rejected",
		},
		{
			name: "invalid_jwt_token",
			setupRequest: func() *http.Request {
				req := httptest.NewRequest("GET", "/test-auth", nil)
				req.Header.Set("Authorization", "Bearer invalid.jwt.token")
				return req
			},
			expectedStatus: http.StatusUnauthorized,
			description:    "Request with invalid JWT token should be rejected",
		},
		{
			name: "malformed_authorization_header",
			setupRequest: func() *http.Request {
				req := httptest.NewRequest("GET", "/test-auth", nil)
				req.Header.Set("Authorization", "NotBearer sometoken")
				return req
			},
			expectedStatus: http.StatusUnauthorized,
			description:    "Request with malformed Authorization header should be rejected",
		},
		{
			name: "expired_jwt_token",
			setupRequest: func() *http.Request {
				// Create an expired JWT token
				token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
					"iss": "https://test-issuer.com",
					"sub": "test-user",
					"exp": time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago
					"iat": time.Now().Add(-2 * time.Hour).Unix(),
				})
				tokenString, _ := token.SignedString([]byte("test-signing-key"))

				req := httptest.NewRequest("GET", "/test-auth", nil)
				req.Header.Set("Authorization", "Bearer "+tokenString)
				return req
			},
			expectedStatus: http.StatusUnauthorized,
			description:    "Request with expired JWT token should be rejected",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := tt.setupRequest()
			recorder := httptest.NewRecorder()
			s3Server.ServeHTTP(recorder, req)

			// Verify the request was rejected with the expected status
			assert.Equal(t, tt.expectedStatus, recorder.Code,
				"%s: expected status %d but got %d. Response: %s",
				tt.description, tt.expectedStatus, recorder.Code, recorder.Body.String())
		})
	}
}

// TestS3IAMOnlyModeRejectsAnonymous tests that when only IAM is configured
// (no traditional identities), anonymous requests are properly denied
func TestS3IAMOnlyModeRejectsAnonymous(t *testing.T) {
	// Create IAM with NO traditional identities (simulating IAM-only setup)
	iam := &IdentityAccessManagement{
		identities:     []*Identity{},
		accessKeyIdent: make(map[string]*Identity),
		nameToIdentity: make(map[string]*Identity),
		accounts:       make(map[string]*Account),
		emailAccount:   make(map[string]*Account),
		hashes:         make(map[string]*sync.Pool),
		hashCounters:   make(map[string]*int32),
	}

	// Set up IAM integration
	iamManager := integration.NewIAMManager()
	config := &integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{Duration: time.Hour},
			MaxSessionLength: sts.FlexibleDuration{Duration: time.Hour * 12},
			Issuer:           "test-sts",
			SigningKey:       []byte("test-signing-key-32-characters-long"),
		},
		Policy: &policy.PolicyEngineConfig{
			DefaultEffect: "Deny",
			StoreType:     "memory",
		},
		Roles: &integration.RoleStoreConfig{
			StoreType: "memory",
		},
	}

	err := iamManager.Initialize(config, func() string {
		return "localhost:8888"
	})
	require.NoError(t, err)

	s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
	require.NotNil(t, s3IAMIntegration)

	// Set IAM integration and explicitly enforce auth, matching what the
	// startup path does when an operator passes -s3.iam.config (#9557).
	iam.SetIAMIntegration(s3IAMIntegration)
	iam.EnableAuthEnforcement()

	// Verify auth is enabled
	require.True(t, iam.isEnabled(), "Auth must be enabled when IAM integration is configured")

	// Test that the Auth middleware blocks unauthenticated requests
	handlerCalled := false
	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handlerCalled = true
		w.WriteHeader(http.StatusOK)
	})

	// Wrap with auth middleware
	wrappedHandler := iam.Auth(testHandler, "Write")

	// Create an unauthenticated request
	req := httptest.NewRequest("PUT", "/mybucket/test.txt", nil)
	rr := httptest.NewRecorder()

	wrappedHandler.ServeHTTP(rr, req)

	// Handler should NOT have been called
	assert.False(t, handlerCalled, "Handler should not be called for unauthenticated request")
	assert.NotEqual(t, http.StatusOK, rr.Code, "Unauthenticated request should not return 200 OK")
}
