package s3api

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/iam/integration"
	"github.com/seaweedfs/seaweedfs/weed/iam/policy"
	"github.com/seaweedfs/seaweedfs/weed/iam/sts"
	"github.com/seaweedfs/seaweedfs/weed/iam/utils"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestS3IAMMiddleware tests the basic S3 IAM middleware functionality
func TestS3IAMMiddleware(t *testing.T) {
	// Create IAM manager
	iamManager := integration.NewIAMManager()

	// Initialize with test configuration
	config := &integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{time.Hour},
			MaxSessionLength: sts.FlexibleDuration{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)

	// Create S3 IAM integration
	s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")

	// Test that integration is created successfully
	assert.NotNil(t, s3IAMIntegration)
	assert.True(t, s3IAMIntegration.enabled)
}

// TestS3IAMMiddlewareJWTAuth tests JWT authentication
func TestS3IAMMiddlewareJWTAuth(t *testing.T) {
	// Skip for now since it requires full setup
	t.Skip("JWT authentication test requires full IAM setup")

	// Create IAM integration
	s3iam := NewS3IAMIntegration(nil, "localhost:8888") // Disabled integration

	// Create test request with JWT token
	req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
	req.Header.Set("Authorization", "Bearer test-token")

	// Test authentication (should return not implemented when disabled)
	ctx := context.Background()
	identity, errCode := s3iam.AuthenticateJWT(ctx, req)

	assert.Nil(t, identity)
	assert.NotEqual(t, errCode, 0) // Should return an error
}

// TestBuildS3ResourceArn tests resource ARN building
func TestBuildS3ResourceArn(t *testing.T) {
	tests := []struct {
		name     string
		bucket   string
		object   string
		expected string
	}{
		{
			name:     "empty bucket and object",
			bucket:   "",
			object:   "",
			expected: "arn:seaweed:s3:::*",
		},
		{
			name:     "bucket only",
			bucket:   "test-bucket",
			object:   "",
			expected: "arn:seaweed:s3:::test-bucket",
		},
		{
			name:     "bucket and object",
			bucket:   "test-bucket",
			object:   "test-object.txt",
			expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
		},
		{
			name:     "bucket and object with leading slash",
			bucket:   "test-bucket",
			object:   "/test-object.txt",
			expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
		},
		{
			name:     "bucket and nested object",
			bucket:   "test-bucket",
			object:   "folder/subfolder/test-object.txt",
			expected: "arn:seaweed:s3:::test-bucket/folder/subfolder/test-object.txt",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := buildS3ResourceArn(tt.bucket, tt.object)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestDetermineGranularS3Action tests granular S3 action determination from HTTP requests
func TestDetermineGranularS3Action(t *testing.T) {
	tests := []struct {
		name           string
		method         string
		bucket         string
		objectKey      string
		queryParams    map[string]string
		fallbackAction Action
		expected       string
		description    string
	}{
		// Object-level operations
		{
			name:           "get_object",
			method:         "GET",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{},
			fallbackAction: s3_constants.ACTION_READ,
			expected:       "s3:GetObject",
			description:    "Basic object retrieval",
		},
		{
			name:           "get_object_acl",
			method:         "GET",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{"acl": ""},
			fallbackAction: s3_constants.ACTION_READ_ACP,
			expected:       "s3:GetObjectAcl",
			description:    "Object ACL retrieval",
		},
		{
			name:           "get_object_tagging",
			method:         "GET",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{"tagging": ""},
			fallbackAction: s3_constants.ACTION_TAGGING,
			expected:       "s3:GetObjectTagging",
			description:    "Object tagging retrieval",
		},
		{
			name:           "put_object",
			method:         "PUT",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{},
			fallbackAction: s3_constants.ACTION_WRITE,
			expected:       "s3:PutObject",
			description:    "Basic object upload",
		},
		{
			name:           "put_object_acl",
			method:         "PUT",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{"acl": ""},
			fallbackAction: s3_constants.ACTION_WRITE_ACP,
			expected:       "s3:PutObjectAcl",
			description:    "Object ACL modification",
		},
		{
			name:           "delete_object",
			method:         "DELETE",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{},
			fallbackAction: s3_constants.ACTION_WRITE, // DELETE object uses WRITE fallback
			expected:       "s3:DeleteObject",
			description:    "Object deletion - correctly mapped to DeleteObject (not PutObject)",
		},
		{
			name:           "delete_object_tagging",
			method:         "DELETE",
			bucket:         "test-bucket",
			objectKey:      "test-object.txt",
			queryParams:    map[string]string{"tagging": ""},
			fallbackAction: s3_constants.ACTION_TAGGING,
			expected:       "s3:DeleteObjectTagging",
			description:    "Object tag deletion",
		},

		// Multipart upload operations
		{
			name:           "create_multipart_upload",
			method:         "POST",
			bucket:         "test-bucket",
			objectKey:      "large-file.txt",
			queryParams:    map[string]string{"uploads": ""},
			fallbackAction: s3_constants.ACTION_WRITE,
			expected:       "s3:CreateMultipartUpload",
			description:    "Multipart upload initiation",
		},
		{
			name:           "upload_part",
			method:         "PUT",
			bucket:         "test-bucket",
			objectKey:      "large-file.txt",
			queryParams:    map[string]string{"uploadId": "12345", "partNumber": "1"},
			fallbackAction: s3_constants.ACTION_WRITE,
			expected:       "s3:UploadPart",
			description:    "Multipart part upload",
		},
		{
			name:           "complete_multipart_upload",
			method:         "POST",
			bucket:         "test-bucket",
			objectKey:      "large-file.txt",
			queryParams:    map[string]string{"uploadId": "12345"},
			fallbackAction: s3_constants.ACTION_WRITE,
			expected:       "s3:CompleteMultipartUpload",
			description:    "Multipart upload completion",
		},
		{
			name:           "abort_multipart_upload",
			method:         "DELETE",
			bucket:         "test-bucket",
			objectKey:      "large-file.txt",
			queryParams:    map[string]string{"uploadId": "12345"},
			fallbackAction: s3_constants.ACTION_WRITE,
			expected:       "s3:AbortMultipartUpload",
			description:    "Multipart upload abort",
		},

		// Bucket-level operations
		{
			name:           "list_bucket",
			method:         "GET",
			bucket:         "test-bucket",
			objectKey:      "",
			queryParams:    map[string]string{},
			fallbackAction: s3_constants.ACTION_LIST,
			expected:       "s3:ListBucket",
			description:    "Bucket listing",
		},
		{
			name:           "get_bucket_acl",
			method:         "GET",
			bucket:         "test-bucket",
			objectKey:      "",
			queryParams:    map[string]string{"acl": ""},
			fallbackAction: s3_constants.ACTION_READ_ACP,
			expected:       "s3:GetBucketAcl",
			description:    "Bucket ACL retrieval",
		},
		{
			name:           "put_bucket_policy",
			method:         "PUT",
			bucket:         "test-bucket",
			objectKey:      "",
			queryParams:    map[string]string{"policy": ""},
			fallbackAction: s3_constants.ACTION_WRITE,
			expected:       "s3:PutBucketPolicy",
			description:    "Bucket policy modification",
		},
		{
			name:           "delete_bucket",
			method:         "DELETE",
			bucket:         "test-bucket",
			objectKey:      "",
			queryParams:    map[string]string{},
			fallbackAction: s3_constants.ACTION_DELETE_BUCKET,
			expected:       "s3:DeleteBucket",
			description:    "Bucket deletion",
		},
		{
			name:           "list_multipart_uploads",
			method:         "GET",
			bucket:         "test-bucket",
			objectKey:      "",
			queryParams:    map[string]string{"uploads": ""},
			fallbackAction: s3_constants.ACTION_LIST,
			expected:       "s3:ListMultipartUploads",
			description:    "List multipart uploads in bucket",
		},

		// Fallback scenarios
		{
			name:           "legacy_read_fallback",
			method:         "GET",
			bucket:         "",
			objectKey:      "",
			queryParams:    map[string]string{},
			fallbackAction: s3_constants.ACTION_READ,
			expected:       "s3:GetObject",
			description:    "Legacy read action fallback",
		},
		{
			name:           "already_granular_action",
			method:         "GET",
			bucket:         "",
			objectKey:      "",
			queryParams:    map[string]string{},
			fallbackAction: "s3:GetBucketLocation", // Already granular
			expected:       "s3:GetBucketLocation",
			description:    "Already granular action passed through",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create HTTP request with query parameters
			req := &http.Request{
				Method: tt.method,
				URL:    &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
			}

			// Add query parameters
			query := req.URL.Query()
			for key, value := range tt.queryParams {
				query.Set(key, value)
			}
			req.URL.RawQuery = query.Encode()

			// Test the granular action determination
			result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)

			assert.Equal(t, tt.expected, result,
				"Test %s failed: %s. Expected %s but got %s",
				tt.name, tt.description, tt.expected, result)
		})
	}
}

// TestMapLegacyActionToIAM tests the legacy action fallback mapping
func TestMapLegacyActionToIAM(t *testing.T) {
	tests := []struct {
		name         string
		legacyAction Action
		expected     string
	}{
		{
			name:         "read_action_fallback",
			legacyAction: s3_constants.ACTION_READ,
			expected:     "s3:GetObject",
		},
		{
			name:         "write_action_fallback",
			legacyAction: s3_constants.ACTION_WRITE,
			expected:     "s3:PutObject",
		},
		{
			name:         "admin_action_fallback",
			legacyAction: s3_constants.ACTION_ADMIN,
			expected:     "s3:*",
		},
		{
			name:         "granular_multipart_action",
			legacyAction: s3_constants.ACTION_CREATE_MULTIPART_UPLOAD,
			expected:     "s3:CreateMultipartUpload",
		},
		{
			name:         "unknown_action_with_s3_prefix",
			legacyAction: "s3:CustomAction",
			expected:     "s3:CustomAction",
		},
		{
			name:         "unknown_action_without_prefix",
			legacyAction: "CustomAction",
			expected:     "s3:CustomAction",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := mapLegacyActionToIAM(tt.legacyAction)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestExtractSourceIP tests source IP extraction from requests
func TestExtractSourceIP(t *testing.T) {
	tests := []struct {
		name       string
		setupReq   func() *http.Request
		expectedIP string
	}{
		{
			name: "X-Forwarded-For header",
			setupReq: func() *http.Request {
				req := httptest.NewRequest("GET", "/test", http.NoBody)
				req.Header.Set("X-Forwarded-For", "192.168.1.100, 10.0.0.1")
				return req
			},
			expectedIP: "192.168.1.100",
		},
		{
			name: "X-Real-IP header",
			setupReq: func() *http.Request {
				req := httptest.NewRequest("GET", "/test", http.NoBody)
				req.Header.Set("X-Real-IP", "192.168.1.200")
				return req
			},
			expectedIP: "192.168.1.200",
		},
		{
			name: "RemoteAddr fallback",
			setupReq: func() *http.Request {
				req := httptest.NewRequest("GET", "/test", http.NoBody)
				req.RemoteAddr = "192.168.1.300:12345"
				return req
			},
			expectedIP: "192.168.1.300",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := tt.setupReq()
			result := extractSourceIP(req)
			assert.Equal(t, tt.expectedIP, result)
		})
	}
}

// TestExtractRoleNameFromPrincipal tests role name extraction
func TestExtractRoleNameFromPrincipal(t *testing.T) {
	tests := []struct {
		name      string
		principal string
		expected  string
	}{
		{
			name:      "valid assumed role ARN",
			principal: "arn:seaweed:sts::assumed-role/S3ReadOnlyRole/session-123",
			expected:  "S3ReadOnlyRole",
		},
		{
			name:      "invalid format",
			principal: "invalid-principal",
			expected:  "", // Returns empty string to signal invalid format
		},
		{
			name:      "missing session name",
			principal: "arn:seaweed:sts::assumed-role/TestRole",
			expected:  "TestRole", // Extracts role name even without session name
		},
		{
			name:      "empty principal",
			principal: "",
			expected:  "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := utils.ExtractRoleNameFromPrincipal(tt.principal)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestIAMIdentityIsAdmin tests the IsAdmin method
func TestIAMIdentityIsAdmin(t *testing.T) {
	identity := &IAMIdentity{
		Name:         "test-identity",
		Principal:    "arn:seaweed:sts::assumed-role/TestRole/session",
		SessionToken: "test-token",
	}

	// In our implementation, IsAdmin always returns false since admin status
	// is determined by policies, not identity
	result := identity.IsAdmin()
	assert.False(t, result)
}
