package s3api

import (
	"context"
	"fmt"
	"net/http"
	"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/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestAssumeRole_CallerIdentityFallback tests the fallback logic when RoleArn is missing
func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
	// Setup STS service
	stsService, _ := setupTestSTSService(t)

	// Create IAM integration mock
	iamMock := &MockIAMIntegration{
		authorizeFunc: func(ctx context.Context, identity *IAMIdentity, action Action, bucket, object string, r *http.Request) s3err.ErrorCode {
			// Allow global sts:AssumeRole
			if action == "sts:AssumeRole" {
				return s3err.ErrNone
			}
			return s3err.ErrAccessDenied
		},
		validateTrustPolicyFunc: func(ctx context.Context, roleArn, principalArn string) error {
			// Allow all trust policies for this test
			return nil
		},
	}

	// Create IAM service with the mock integration
	iam := &IdentityAccessManagement{
		iamIntegration: iamMock,
	}

	// Create STS handlers
	stsHandlers := NewSTSHandlers(stsService, iam)

	// Test case 1: Caller is an IAM User, RoleArn is missing
	t.Run("Caller is IAM User, No RoleArn", func(t *testing.T) {
		// Mock request
		req, err := http.NewRequest("POST", "/", nil)
		require.NoError(t, err)
		req.Form = url.Values{}
		req.Form.Set("Action", "AssumeRole")
		req.Form.Set("RoleSessionName", "test-session")
		req.Form.Set("Version", "2011-06-15")

		// Mock the authenticated identity (IAM User)
		callerIdentity := &Identity{
			Name:         "alice",
			Account:      &AccountAdmin,
			PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/alice", defaultAccountID),
			Actions:      []Action{s3_constants.ACTION_ADMIN},
		}

		// 1. Test prepareSTSCredentials with NO RoleArn (simulating the fallback logic having passed PrincipalArn)
		// expected RoleArn passed to prepareSTSCredentials would be the caller's PrincipalArn
		fallbackRoleArn := callerIdentity.PrincipalArn

		// Prepare custom claims for the session (mimicking handleAssumeRole logic)
		var modifyClaims func(claims *sts.STSSessionClaims)
		if callerIdentity.isAdmin() {
			modifyClaims = func(claims *sts.STSSessionClaims) {
				if claims.RequestContext == nil {
					claims.RequestContext = make(map[string]interface{})
				}
				claims.RequestContext["is_admin"] = true
			}
		}

		stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), fallbackRoleArn, "test-session", nil, "", modifyClaims)
		require.NoError(t, err)

		// Assertions
		// The role name should be extracted from the user ARN ("alice")
		assert.Contains(t, assumedUser.Arn, fmt.Sprintf("assumed-role/alice/test-session"))
		assert.Contains(t, assumedUser.AssumedRoleId, "alice:test-session")

		// Verify token claims using ValidateSessionToken
		sessionInfo, err := stsService.ValidateSessionToken(context.Background(), stsCreds.SessionToken)
		require.NoError(t, err)

		// The RoleArn in session info should match the fallback ARN (user ARN)
		assert.Equal(t, fallbackRoleArn, sessionInfo.RoleArn)

		// Verify is_admin claim is present
		isAdmin, ok := sessionInfo.RequestContext["is_admin"].(bool)
		assert.True(t, ok, "is_admin claim should be present")
		assert.True(t, isAdmin, "is_admin claim should be true")
	})

	// Test case 2: Caller is an STS Assumed Role, No RoleArn
	t.Run("Caller is STS Assumed Role, No RoleArn", func(t *testing.T) {
		// Mock identity
		callerIdentity := &Identity{
			Name:         "arn:aws:sts::111122223333:assumed-role/admin/session1",
			Account:      &AccountAdmin,
			PrincipalArn: "arn:aws:sts::111122223333:assumed-role/admin/session1",
		}

		fallbackRoleArn := callerIdentity.PrincipalArn

		stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), fallbackRoleArn, "nested-session", nil, "", nil)
		require.NoError(t, err)

		// The role name should be extracted from the assumed role ARN ("admin")
		assert.Contains(t, assumedUser.Arn, "assumed-role/admin/nested-session")
		assert.Contains(t, assumedUser.AssumedRoleId, "admin:nested-session")

		// Check claims
		sessionInfo, err := stsService.ValidateSessionToken(context.Background(), stsCreds.SessionToken)
		require.NoError(t, err)
		assert.Equal(t, fallbackRoleArn, sessionInfo.RoleArn)
	})

	// Test case 3: Explicit RoleArn provided (Standard AssumeRole)
	t.Run("Explicit RoleArn Provided", func(t *testing.T) {
		explicitRoleArn := "arn:aws:iam::111122223333:role/TargetRole"

		stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), explicitRoleArn, "explicit-session", nil, "", nil)
		require.NoError(t, err)

		// Role name should be "TargetRole"
		assert.Contains(t, assumedUser.Arn, "assumed-role/TargetRole/explicit-session")

		// Check claims
		sessionInfo, err := stsService.ValidateSessionToken(context.Background(), stsCreds.SessionToken)
		require.NoError(t, err)
		assert.Equal(t, explicitRoleArn, sessionInfo.RoleArn)
	})

	// Test case 4: Malformed ARN (Edge case)
	t.Run("Malformed ARN", func(t *testing.T) {
		malformedArn := "invalid-arn"

		stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), malformedArn, "bad-session", nil, "", nil)
		require.NoError(t, err)

		// Fallback behavior: use full string as role name if extraction fails
		assert.Contains(t, assumedUser.Arn, "assumed-role/invalid-arn/bad-session")

		sessionInfo, err := stsService.ValidateSessionToken(context.Background(), stsCreds.SessionToken)
		require.NoError(t, err)
		assert.Equal(t, malformedArn, sessionInfo.RoleArn)
	})
}

func TestAssumeRole_EmbedsRolePolicies(t *testing.T) {
	t.Run("RoleWithAttachedPolicies", func(t *testing.T) {
		ctx := context.Background()
		manager := newTestSTSIntegrationManager(t)

		writePolicy := &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{
					Effect: "Allow",
					Action: []string{"s3:*"},
					Resource: []string{
						"arn:aws:s3:::*",
						"arn:aws:s3:::*/*",
					},
				},
			},
		}
		require.NoError(t, manager.CreatePolicy(ctx, "", "S3WritePolicy", writePolicy))

		roleName := "LakekeeperVendedRole"
		require.NoError(t, manager.CreateRole(ctx, "", roleName, &integration.RoleDefinition{
			RoleName:         roleName,
			AttachedPolicies: []string{"S3WritePolicy"},
		}))

		iam := &IdentityAccessManagement{
			iamIntegration: NewS3IAMIntegration(manager, ""),
		}
		stsHandlers := NewSTSHandlers(manager.GetSTSService(), iam)

		roleArn := fmt.Sprintf("arn:aws:iam::%s:role/%s", defaultAccountID, roleName)
		stsCreds, _, err := stsHandlers.prepareSTSCredentials(ctx, roleArn, "test-session", nil, "", nil)
		require.NoError(t, err)

		sessionInfo, err := manager.GetSTSService().ValidateSessionToken(ctx, stsCreds.SessionToken)
		require.NoError(t, err)
		require.NotNil(t, sessionInfo)
		assert.Equal(t, []string{"S3WritePolicy"}, sessionInfo.Policies)
	})

	t.Run("RoleWithoutAttachedPolicies", func(t *testing.T) {
		ctx := context.Background()
		manager := newTestSTSIntegrationManager(t)

		roleName := "LakekeeperEmptyRole"
		require.NoError(t, manager.CreateRole(ctx, "", roleName, &integration.RoleDefinition{
			RoleName: roleName,
		}))

		iam := &IdentityAccessManagement{
			iamIntegration: NewS3IAMIntegration(manager, ""),
		}
		stsHandlers := NewSTSHandlers(manager.GetSTSService(), iam)

		roleArn := fmt.Sprintf("arn:aws:iam::%s:role/%s", defaultAccountID, roleName)
		stsCreds, _, err := stsHandlers.prepareSTSCredentials(ctx, roleArn, "test-session", nil, "", nil)
		require.NoError(t, err)

		sessionInfo, err := manager.GetSTSService().ValidateSessionToken(ctx, stsCreds.SessionToken)
		require.NoError(t, err)
		assert.Empty(t, sessionInfo.Policies)
	})
}

func newTestSTSIntegrationManager(t *testing.T) *integration.IAMManager {
	t.Helper()
	manager := integration.NewIAMManager()
	config := &integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{Duration: time.Hour},
			MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
			Issuer:           "test-issuer",
			SigningKey:       []byte("test-signing-key-at-least-32-bytes-long-for-security"),
		},
		Policy: &policy.PolicyEngineConfig{
			DefaultEffect: "Deny",
			StoreType:     "memory",
		},
		Roles: &integration.RoleStoreConfig{
			StoreType: "memory",
		},
	}
	require.NoError(t, manager.Initialize(config, func() string { return "" }))
	return manager
}
