package s3api

import (
	"context"
	"encoding/xml"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"sort"
	"strings"
	"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/pb/iam_pb"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// mockUserStore implements integration.UserStore for testing GetPoliciesForUser
type mockUserStore struct {
	users map[string]*iam_pb.Identity
}

func (m *mockUserStore) GetUser(_ context.Context, username string) (*iam_pb.Identity, error) {
	u, ok := m.users[username]
	if !ok {
		return nil, nil
	}
	return u, nil
}

// TestGetFederationToken_BasicFlow tests basic credential generation for GetFederationToken
func TestGetFederationToken_BasicFlow(t *testing.T) {
	stsService, _ := setupTestSTSService(t)

	iam := &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	}
	stsHandlers := NewSTSHandlers(stsService, iam)

	// Simulate the core logic of handleGetFederationToken
	name := "BobApp"
	callerIdentity := &Identity{
		Name:         "alice",
		PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/alice", defaultAccountID),
		PolicyNames:  []string{"S3ReadPolicy"},
	}

	accountID := stsHandlers.getAccountID()

	// Generate session ID and credentials
	sessionId, err := sts.GenerateSessionId()
	require.NoError(t, err)

	expiration := time.Now().Add(12 * time.Hour)
	federatedUserArn := fmt.Sprintf("arn:aws:sts::%s:federated-user/%s", accountID, name)
	federatedUserId := fmt.Sprintf("%s:%s", accountID, name)

	claims := sts.NewSTSSessionClaims(sessionId, stsService.Config.Issuer, expiration).
		WithSessionName(name).
		WithRoleInfo(callerIdentity.PrincipalArn, federatedUserId, federatedUserArn).
		WithPolicies(callerIdentity.PolicyNames)

	sessionToken, err := stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
	require.NoError(t, err)

	// Validate the session token
	sessionInfo, err := stsService.ValidateSessionToken(context.Background(), sessionToken)
	require.NoError(t, err)
	require.NotNil(t, sessionInfo)

	// Verify the session info contains caller's policies
	assert.Equal(t, []string{"S3ReadPolicy"}, sessionInfo.Policies)

	// Verify principal is the federated user ARN
	assert.Equal(t, federatedUserArn, sessionInfo.Principal)

	// Verify the RoleArn points to the caller's identity (for policy resolution)
	assert.Equal(t, callerIdentity.PrincipalArn, sessionInfo.RoleArn)

	// Verify session name
	assert.Equal(t, name, sessionInfo.SessionName)
}

// TestGetFederationToken_WithSessionPolicy tests session policy scoping
func TestGetFederationToken_WithSessionPolicy(t *testing.T) {
	stsService, _ := setupTestSTSService(t)

	stsHandlers := NewSTSHandlers(stsService, &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	})

	accountID := stsHandlers.getAccountID()
	name := "ScopedApp"

	sessionPolicyJSON := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::my-bucket/*"]}]}`
	normalizedPolicy, err := sts.NormalizeSessionPolicy(sessionPolicyJSON)
	require.NoError(t, err)

	sessionId, err := sts.GenerateSessionId()
	require.NoError(t, err)

	expiration := time.Now().Add(12 * time.Hour)
	federatedUserArn := fmt.Sprintf("arn:aws:sts::%s:federated-user/%s", accountID, name)
	federatedUserId := fmt.Sprintf("%s:%s", accountID, name)

	claims := sts.NewSTSSessionClaims(sessionId, stsService.Config.Issuer, expiration).
		WithSessionName(name).
		WithRoleInfo("arn:aws:iam::000000000000:user/caller", federatedUserId, federatedUserArn).
		WithPolicies([]string{"S3FullAccess"}).
		WithSessionPolicy(normalizedPolicy)

	sessionToken, err := stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
	require.NoError(t, err)

	sessionInfo, err := stsService.ValidateSessionToken(context.Background(), sessionToken)
	require.NoError(t, err)
	require.NotNil(t, sessionInfo)

	// Verify session policy is embedded
	assert.NotEmpty(t, sessionInfo.SessionPolicy)
	assert.Contains(t, sessionInfo.SessionPolicy, "s3:GetObject")

	// Verify caller's policies are still present
	assert.Equal(t, []string{"S3FullAccess"}, sessionInfo.Policies)
}

// TestGetFederationToken_RejectTemporaryCredentials tests that requests with
// session tokens are rejected.
func TestGetFederationToken_RejectTemporaryCredentials(t *testing.T) {
	stsService, _ := setupTestSTSService(t)
	stsHandlers := NewSTSHandlers(stsService, &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	})

	tests := []struct {
		name        string
		setToken    func(r *http.Request)
		description string
	}{
		{
			name: "SessionTokenInHeader",
			setToken: func(r *http.Request) {
				r.Header.Set("X-Amz-Security-Token", "some-session-token")
			},
			description: "Session token in X-Amz-Security-Token header should be rejected",
		},
		{
			name: "SessionTokenInQuery",
			setToken: func(r *http.Request) {
				q := r.URL.Query()
				q.Set("X-Amz-Security-Token", "some-session-token")
				r.URL.RawQuery = q.Encode()
			},
			description: "Session token in query string should be rejected",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			form := url.Values{}
			form.Set("Action", "GetFederationToken")
			form.Set("Name", "TestUser")
			form.Set("Version", "2011-06-15")

			req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
			tt.setToken(req)

			// Parse form so the handler can read it
			require.NoError(t, req.ParseForm())
			// Re-set values after parse
			req.Form.Set("Action", "GetFederationToken")
			req.Form.Set("Name", "TestUser")
			req.Form.Set("Version", "2011-06-15")

			rr := httptest.NewRecorder()
			stsHandlers.HandleSTSRequest(rr, req)

			// The handler rejects temporary credentials before SigV4 verification
			assert.Equal(t, http.StatusForbidden, rr.Code, tt.description)
			assert.Contains(t, rr.Body.String(), "AccessDenied")
			assert.Contains(t, rr.Body.String(), "cannot be called with temporary credentials")
		})
	}
}

// TestGetFederationToken_MissingName tests that a missing Name parameter returns an error
func TestGetFederationToken_MissingName(t *testing.T) {
	stsService, _ := setupTestSTSService(t)
	stsHandlers := NewSTSHandlers(stsService, &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	})

	req := httptest.NewRequest("POST", "/", nil)
	req.Form = url.Values{}
	req.Form.Set("Action", "GetFederationToken")
	req.Form.Set("Version", "2011-06-15")
	// Name is intentionally omitted

	rr := httptest.NewRecorder()
	stsHandlers.HandleSTSRequest(rr, req)

	assert.Equal(t, http.StatusBadRequest, rr.Code)
	assert.Contains(t, rr.Body.String(), "Name is required")
}

// TestGetFederationToken_NameValidation tests Name parameter validation
func TestGetFederationToken_NameValidation(t *testing.T) {
	stsService, _ := setupTestSTSService(t)
	stsHandlers := NewSTSHandlers(stsService, &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	})

	tests := []struct {
		name        string
		federName   string
		expectError bool
		errContains string
	}{
		{
			name:        "TooShort",
			federName:   "A",
			expectError: true,
			errContains: "between 2 and 32",
		},
		{
			name:        "TooLong",
			federName:   strings.Repeat("A", 33),
			expectError: true,
			errContains: "between 2 and 32",
		},
		{
			name:        "MinLength",
			federName:   "AB",
			expectError: false,
		},
		{
			name:        "MaxLength",
			federName:   strings.Repeat("A", 32),
			expectError: false,
		},
		{
			name:        "ValidSpecialChars",
			federName:   "user+=,.@-test",
			expectError: false,
		},
		{
			name:        "InvalidChars_Space",
			federName:   "bad name",
			expectError: true,
			errContains: "invalid characters",
		},
		{
			name:        "InvalidChars_Slash",
			federName:   "bad/name",
			expectError: true,
			errContains: "invalid characters",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest("POST", "/", nil)
			req.Form = url.Values{}
			req.Form.Set("Action", "GetFederationToken")
			req.Form.Set("Name", tt.federName)
			req.Form.Set("Version", "2011-06-15")

			rr := httptest.NewRecorder()
			stsHandlers.HandleSTSRequest(rr, req)

			if tt.expectError {
				assert.Equal(t, http.StatusBadRequest, rr.Code)
				assert.Contains(t, rr.Body.String(), tt.errContains)
			} else {
				// Valid name should proceed past validation — will fail at SigV4
				// (returns 403 because we have no real signature)
				assert.NotEqual(t, http.StatusBadRequest, rr.Code,
					"Valid name should not produce a 400 for name validation")
			}
		})
	}
}

// TestGetFederationToken_DurationValidation tests DurationSeconds validation
func TestGetFederationToken_DurationValidation(t *testing.T) {
	stsService, _ := setupTestSTSService(t)
	stsHandlers := NewSTSHandlers(stsService, &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	})

	tests := []struct {
		name        string
		duration    string
		expectError bool
		errContains string
	}{
		{
			name:        "BelowMinimum",
			duration:    "899",
			expectError: true,
			errContains: "between",
		},
		{
			name:        "AboveMaximum",
			duration:    "129601",
			expectError: true,
			errContains: "between",
		},
		{
			name:        "InvalidFormat",
			duration:    "not-a-number",
			expectError: true,
			errContains: "invalid DurationSeconds",
		},
		{
			name:        "MinimumValid",
			duration:    "900",
			expectError: false,
		},
		{
			name:        "MaximumValid_36Hours",
			duration:    "129600",
			expectError: false,
		},
		{
			name:        "Default12Hours",
			duration:    "43200",
			expectError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest("POST", "/", nil)
			req.Form = url.Values{}
			req.Form.Set("Action", "GetFederationToken")
			req.Form.Set("Name", "TestUser")
			req.Form.Set("DurationSeconds", tt.duration)
			req.Form.Set("Version", "2011-06-15")

			rr := httptest.NewRecorder()
			stsHandlers.HandleSTSRequest(rr, req)

			if tt.expectError {
				assert.Equal(t, http.StatusBadRequest, rr.Code)
				assert.Contains(t, rr.Body.String(), tt.errContains)
			} else {
				// Valid duration should proceed past validation — will fail at SigV4
				assert.NotEqual(t, http.StatusBadRequest, rr.Code,
					"Valid duration should not produce a 400 for duration validation")
			}
		})
	}
}

// TestGetFederationToken_ResponseFormat tests the XML response structure
func TestGetFederationToken_ResponseFormat(t *testing.T) {
	// Verify the response XML structure matches AWS format
	response := GetFederationTokenResponse{
		Result: GetFederationTokenResult{
			Credentials: STSCredentials{
				AccessKeyId:     "ASIA1234567890",
				SecretAccessKey: "secret123",
				SessionToken:    "token123",
				Expiration:      "2026-04-02T12:00:00Z",
			},
			FederatedUser: FederatedUser{
				FederatedUserId: "000000000000:BobApp",
				Arn:             "arn:aws:sts::000000000000:federated-user/BobApp",
			},
		},
	}
	response.ResponseMetadata.RequestId = "test-request-id"

	data, err := xml.MarshalIndent(response, "", "  ")
	require.NoError(t, err)

	xmlStr := string(data)
	assert.Contains(t, xmlStr, "GetFederationTokenResponse")
	assert.Contains(t, xmlStr, "GetFederationTokenResult")
	assert.Contains(t, xmlStr, "FederatedUser")
	assert.Contains(t, xmlStr, "FederatedUserId")
	assert.Contains(t, xmlStr, "federated-user/BobApp")
	assert.Contains(t, xmlStr, "ASIA1234567890")
	assert.Contains(t, xmlStr, "test-request-id")

	// Verify it can be unmarshaled back
	var parsed GetFederationTokenResponse
	err = xml.Unmarshal(data, &parsed)
	require.NoError(t, err)
	assert.Equal(t, "ASIA1234567890", parsed.Result.Credentials.AccessKeyId)
	assert.Equal(t, "arn:aws:sts::000000000000:federated-user/BobApp", parsed.Result.FederatedUser.Arn)
	assert.Equal(t, "000000000000:BobApp", parsed.Result.FederatedUser.FederatedUserId)
}

// TestGetFederationToken_PolicyEmbedding tests that the caller's policies are embedded
// into the session token using the IAM integration manager
func TestGetFederationToken_PolicyEmbedding(t *testing.T) {
	ctx := context.Background()
	manager := newTestSTSIntegrationManager(t)

	// Create a policy that the user has attached
	userPolicy := &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{
			{
				Effect:   "Allow",
				Action:   []string{"s3:GetObject", "s3:PutObject"},
				Resource: []string{"arn:aws:s3:::user-bucket/*"},
			},
		},
	}
	require.NoError(t, manager.CreatePolicy(ctx, "", "UserS3Policy", userPolicy))

	stsService := manager.GetSTSService()

	// Simulate what handleGetFederationToken does for policy embedding
	name := "AppClient"
	callerPolicies := []string{"UserS3Policy"}

	sessionId, err := sts.GenerateSessionId()
	require.NoError(t, err)

	expiration := time.Now().Add(12 * time.Hour)
	accountID := defaultAccountID
	federatedUserArn := fmt.Sprintf("arn:aws:sts::%s:federated-user/%s", accountID, name)
	federatedUserId := fmt.Sprintf("%s:%s", accountID, name)

	claims := sts.NewSTSSessionClaims(sessionId, stsService.Config.Issuer, expiration).
		WithSessionName(name).
		WithRoleInfo("arn:aws:iam::000000000000:user/caller", federatedUserId, federatedUserArn).
		WithPolicies(callerPolicies)

	sessionToken, err := stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
	require.NoError(t, err)

	sessionInfo, err := stsService.ValidateSessionToken(ctx, sessionToken)
	require.NoError(t, err)
	require.NotNil(t, sessionInfo)

	// Verify the caller's policy names are embedded
	assert.Equal(t, []string{"UserS3Policy"}, sessionInfo.Policies)
}

// TestGetFederationToken_PolicyIntersection tests that both the caller's base policies
// and the restrictive session policy are embedded in the token, enabling the
// authorization layer to compute their intersection at request time.
func TestGetFederationToken_PolicyIntersection(t *testing.T) {
	ctx := context.Background()
	manager := newTestSTSIntegrationManager(t)

	// Create a broad policy for the caller
	broadPolicy := &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, "", "S3FullAccess", broadPolicy))

	stsService := manager.GetSTSService()

	// Session policy restricts to one bucket and one action
	sessionPolicyJSON := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::restricted-bucket/*"]}]}`
	normalizedPolicy, err := sts.NormalizeSessionPolicy(sessionPolicyJSON)
	require.NoError(t, err)

	sessionId, err := sts.GenerateSessionId()
	require.NoError(t, err)

	expiration := time.Now().Add(12 * time.Hour)
	name := "RestrictedApp"
	accountID := defaultAccountID
	federatedUserArn := fmt.Sprintf("arn:aws:sts::%s:federated-user/%s", accountID, name)
	federatedUserId := fmt.Sprintf("%s:%s", accountID, name)

	claims := sts.NewSTSSessionClaims(sessionId, stsService.Config.Issuer, expiration).
		WithSessionName(name).
		WithRoleInfo("arn:aws:iam::000000000000:user/caller", federatedUserId, federatedUserArn).
		WithPolicies([]string{"S3FullAccess"}).
		WithSessionPolicy(normalizedPolicy)

	sessionToken, err := stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
	require.NoError(t, err)

	sessionInfo, err := stsService.ValidateSessionToken(ctx, sessionToken)
	require.NoError(t, err)
	require.NotNil(t, sessionInfo)

	// Verify both the broad base policies and the restrictive session policy are embedded
	// The authorization layer computes intersection at request time
	assert.Equal(t, []string{"S3FullAccess"}, sessionInfo.Policies,
		"Caller's base policies should be embedded in token")
	assert.Contains(t, sessionInfo.SessionPolicy, "restricted-bucket",
		"Session policy should restrict to specific bucket")
	assert.Contains(t, sessionInfo.SessionPolicy, "s3:GetObject",
		"Session policy should restrict to specific action")
}

// TestGetFederationToken_MalformedPolicy tests that invalid policy JSON is rejected
// by the session policy normalization used in the handler
func TestGetFederationToken_MalformedPolicy(t *testing.T) {
	tests := []struct {
		name      string
		policyStr string
		expectErr bool
	}{
		{
			name:      "InvalidJSON",
			policyStr: "not-valid-json",
			expectErr: true,
		},
		{
			name:      "EmptyObject",
			policyStr: "{}",
			expectErr: true,
		},
		{
			name:      "TooLarge",
			policyStr: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["` + strings.Repeat("a", 2048) + `"]}]}`,
			expectErr: true,
		},
		{
			name:      "ValidPolicy",
			policyStr: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"]}]}`,
			expectErr: false,
		},
		{
			name:      "EmptyString",
			policyStr: "",
			expectErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := sts.NormalizeSessionPolicy(tt.policyStr)
			if tt.expectErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// TestGetFederationToken_STSNotReady tests that the handler returns 503 when STS is not initialized
func TestGetFederationToken_STSNotReady(t *testing.T) {
	// Create handlers with nil STS service
	stsHandlers := NewSTSHandlers(nil, &IdentityAccessManagement{
		iamIntegration: &MockIAMIntegration{},
	})

	req := httptest.NewRequest("POST", "/", nil)
	req.Form = url.Values{}
	req.Form.Set("Action", "GetFederationToken")
	req.Form.Set("Name", "TestUser")
	req.Form.Set("Version", "2011-06-15")

	rr := httptest.NewRecorder()
	stsHandlers.HandleSTSRequest(rr, req)

	assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
	assert.Contains(t, rr.Body.String(), "ServiceUnavailable")
}

// TestGetFederationToken_DefaultDuration tests that the default duration is 12 hours
func TestGetFederationToken_DefaultDuration(t *testing.T) {
	assert.Equal(t, int64(43200), defaultFederationDurationSeconds, "Default duration should be 12 hours (43200 seconds)")
	assert.Equal(t, int64(129600), maxFederationDurationSeconds, "Max duration should be 36 hours (129600 seconds)")
}

// TestGetFederationToken_GetPoliciesForUser tests that GetPoliciesForUser
// correctly resolves user policies from the UserStore and returns errors
// when the store is unavailable.
func TestGetFederationToken_GetPoliciesForUser(t *testing.T) {
	ctx := context.Background()
	manager := newTestSTSIntegrationManager(t)

	t.Run("NoUserStore", func(t *testing.T) {
		// UserStore not set — should return error
		policies, err := manager.GetPoliciesForUser(ctx, "alice")
		assert.Error(t, err)
		assert.Nil(t, policies)
		assert.Contains(t, err.Error(), "user store not configured")
	})

	t.Run("UserNotFound", func(t *testing.T) {
		manager.SetUserStore(&mockUserStore{users: map[string]*iam_pb.Identity{}})
		policies, err := manager.GetPoliciesForUser(ctx, "nonexistent")
		assert.NoError(t, err)
		assert.Nil(t, policies)
	})

	t.Run("UserWithPolicies", func(t *testing.T) {
		manager.SetUserStore(&mockUserStore{
			users: map[string]*iam_pb.Identity{
				"alice": {
					Name:        "alice",
					PolicyNames: []string{"GroupReadPolicy", "GroupWritePolicy"},
				},
			},
		})
		policies, err := manager.GetPoliciesForUser(ctx, "alice")
		assert.NoError(t, err)
		assert.Equal(t, []string{"GroupReadPolicy", "GroupWritePolicy"}, policies)
	})

	t.Run("UserWithNoPolicies", func(t *testing.T) {
		manager.SetUserStore(&mockUserStore{
			users: map[string]*iam_pb.Identity{
				"bob": {Name: "bob"},
			},
		})
		policies, err := manager.GetPoliciesForUser(ctx, "bob")
		assert.NoError(t, err)
		assert.Empty(t, policies)
	})
}

// TestGetFederationToken_PolicyMergeAndDedup tests that the handler's policy
// merge logic correctly combines identity.PolicyNames with IAM-manager-resolved
// policies and deduplicates the result.
func TestGetFederationToken_PolicyMergeAndDedup(t *testing.T) {
	ctx := context.Background()
	manager := newTestSTSIntegrationManager(t)

	// Create policies so they exist in the engine
	for _, name := range []string{"DirectPolicy", "GroupPolicy", "SharedPolicy"} {
		require.NoError(t, manager.CreatePolicy(ctx, "", name, &policy.PolicyDocument{
			Version: "2012-10-17",
			Statement: []policy.Statement{
				{Effect: "Allow", Action: []string{"s3:GetObject"}, Resource: []string{"arn:aws:s3:::*/*"}},
			},
		}))
	}

	// Set up a user store that returns group-attached policies
	manager.SetUserStore(&mockUserStore{
		users: map[string]*iam_pb.Identity{
			"alice": {
				Name:        "alice",
				PolicyNames: []string{"GroupPolicy", "SharedPolicy"},
			},
		},
	})

	stsService := manager.GetSTSService()

	// Simulate what the handler does: merge identity.PolicyNames with GetPoliciesForUser
	identityPolicies := []string{"DirectPolicy", "SharedPolicy"} // SharedPolicy overlaps

	policySet := make(map[string]struct{})
	for _, p := range identityPolicies {
		policySet[p] = struct{}{}
	}

	userPolicies, err := manager.GetPoliciesForUser(ctx, "alice")
	require.NoError(t, err)
	for _, p := range userPolicies {
		policySet[p] = struct{}{}
	}

	merged := make([]string, 0, len(policySet))
	for p := range policySet {
		merged = append(merged, p)
	}
	sort.Strings(merged) // deterministic for assertion

	// Should contain all three unique policies, no duplicates
	assert.Equal(t, []string{"DirectPolicy", "GroupPolicy", "SharedPolicy"}, merged)

	// Verify the merged policies can be embedded in a token and recovered
	sessionId, err := sts.GenerateSessionId()
	require.NoError(t, err)

	expiration := time.Now().Add(time.Hour)
	claims := sts.NewSTSSessionClaims(sessionId, stsService.Config.Issuer, expiration).
		WithSessionName("test").
		WithRoleInfo("arn:aws:iam::000000000000:user/alice", "000000000000:test", "arn:aws:sts::000000000000:federated-user/test").
		WithPolicies(merged)

	token, err := stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
	require.NoError(t, err)

	sessionInfo, err := stsService.ValidateSessionToken(ctx, token)
	require.NoError(t, err)

	sort.Strings(sessionInfo.Policies)
	assert.Equal(t, []string{"DirectPolicy", "GroupPolicy", "SharedPolicy"}, sessionInfo.Policies,
		"Token should contain the deduplicated merge of identity and group policies")
}

// TestGetFederationToken_PolicyMergeNoManager tests that when the IAM manager
// is unavailable, identity.PolicyNames alone are still embedded.
func TestGetFederationToken_PolicyMergeNoManager(t *testing.T) {
	ctx := context.Background()
	stsService, _ := setupTestSTSService(t)

	// No IAM manager — only identity.PolicyNames should be used
	identityPolicies := []string{"UserDirectPolicy"}

	policySet := make(map[string]struct{})
	for _, p := range identityPolicies {
		policySet[p] = struct{}{}
	}

	// IAM manager is nil — skip GetPoliciesForUser (mirrors handler logic)
	var policyManager *integration.IAMManager // nil
	if policyManager != nil {
		t.Fatal("policyManager should be nil in this test")
	}

	merged := make([]string, 0, len(policySet))
	for p := range policySet {
		merged = append(merged, p)
	}

	sessionId, err := sts.GenerateSessionId()
	require.NoError(t, err)

	expiration := time.Now().Add(time.Hour)
	claims := sts.NewSTSSessionClaims(sessionId, stsService.Config.Issuer, expiration).
		WithSessionName("test").
		WithRoleInfo("arn:aws:iam::000000000000:user/alice", "000000000000:test", "arn:aws:sts::000000000000:federated-user/test").
		WithPolicies(merged)

	token, err := stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
	require.NoError(t, err)

	sessionInfo, err := stsService.ValidateSessionToken(ctx, token)
	require.NoError(t, err)

	assert.Equal(t, []string{"UserDirectPolicy"}, sessionInfo.Policies,
		"Without IAM manager, only identity policies should be embedded")
}
