// Package permission provides integration tests for the GCP permission sync functionality.
// This file contains property-based integration tests that verify the GCP role mapping
// synchronization workflow.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// For any GCP role mapping changes, the system should automatically synchronize
// affected users' permissions.
//
// **Validates: Requirements 10.4**
package permission

import (
	"context"
	"sync"
	"testing"
	"time"

	"AutoSSO/internal/config"
	"AutoSSO/internal/pkg/logger"
	"AutoSSO/internal/provider"
	"AutoSSO/internal/state"

	"pgregory.net/rapid"
)

// =============================================================================
// Mock Implementations for GCP Sync Integration Testing
// =============================================================================

// gcpSyncMockStateStore is a mock state store for GCP sync integration testing.
type gcpSyncMockStateStore struct {
	instances        map[string]*state.ApprovalInstance
	pendingRemovals  []*state.PendingRemoval
	deletedInstances map[string]bool
	mu               sync.Mutex
	nextRemovalID    int64
}

func newGCPSyncMockStateStore() *gcpSyncMockStateStore {
	return &gcpSyncMockStateStore{
		instances:        make(map[string]*state.ApprovalInstance),
		pendingRemovals:  make([]*state.PendingRemoval, 0),
		deletedInstances: make(map[string]bool),
		nextRemovalID:    1,
	}
}

func (s *gcpSyncMockStateStore) SaveInstance(ctx context.Context, instance *state.ApprovalInstance) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if instance != nil {
		s.instances[instance.ID] = instance
	}
	return nil
}

func (s *gcpSyncMockStateStore) GetInstance(ctx context.Context, instanceID string) (*state.ApprovalInstance, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.instances[instanceID], nil
}

func (s *gcpSyncMockStateStore) ListInstances(ctx context.Context, filter *state.InstanceFilter) ([]*state.ApprovalInstance, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	result := make([]*state.ApprovalInstance, 0, len(s.instances))
	for _, inst := range s.instances {
		if filter != nil {
			if filter.Status != "" && inst.Status != filter.Status {
				continue
			}
			if filter.CloudType != "" && inst.CloudType != filter.CloudType {
				continue
			}
			if filter.Email != "" && inst.Email != filter.Email {
				continue
			}
			if filter.AccountID != "" && inst.AccountID != filter.AccountID {
				continue
			}
		}
		result = append(result, inst)
	}
	return result, nil
}

func (s *gcpSyncMockStateStore) DeleteInstance(ctx context.Context, instanceID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.instances, instanceID)
	return nil
}

func (s *gcpSyncMockStateStore) SavePendingRemoval(ctx context.Context, removal *state.PendingRemoval) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if removal != nil {
		removal.ID = s.nextRemovalID
		s.nextRemovalID++
		s.pendingRemovals = append(s.pendingRemovals, removal)
	}
	return nil
}

func (s *gcpSyncMockStateStore) GetPendingRemovals(ctx context.Context) ([]*state.PendingRemoval, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	result := make([]*state.PendingRemoval, len(s.pendingRemovals))
	copy(result, s.pendingRemovals)
	return result, nil
}

func (s *gcpSyncMockStateStore) DeletePendingRemoval(ctx context.Context, id int64) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i, r := range s.pendingRemovals {
		if r.ID == id {
			s.pendingRemovals = append(s.pendingRemovals[:i], s.pendingRemovals[i+1:]...)
			break
		}
	}
	return nil
}

func (s *gcpSyncMockStateStore) SaveDeletedInstance(ctx context.Context, instanceID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.deletedInstances[instanceID] = true
	return nil
}

func (s *gcpSyncMockStateStore) IsInstanceDeleted(ctx context.Context, instanceID string) (bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.deletedInstances[instanceID], nil
}

func (s *gcpSyncMockStateStore) Close() error {
	return nil
}

// Helper methods for testing
func (s *gcpSyncMockStateStore) getInstancesByProjectAndRole(projectID, roleName string) []*state.ApprovalInstance {
	s.mu.Lock()
	defer s.mu.Unlock()
	var result []*state.ApprovalInstance
	for _, inst := range s.instances {
		if inst.AccountID == projectID && inst.CloudType == string(provider.ProviderGCP) {
			for _, ps := range inst.PermissionSets {
				if ps == roleName {
					result = append(result, inst)
					break
				}
			}
		}
	}
	return result
}

func (s *gcpSyncMockStateStore) getGCPInstances() []*state.ApprovalInstance {
	s.mu.Lock()
	defer s.mu.Unlock()
	var result []*state.ApprovalInstance
	for _, inst := range s.instances {
		if inst.CloudType == string(provider.ProviderGCP) {
			result = append(result, inst)
		}
	}
	return result
}

// =============================================================================
// GCP Sync Mock Provider
// =============================================================================

// gcpSyncMockProvider is a mock GCP provider that tracks role binding operations
// and simulates role mapping changes.
type gcpSyncMockProvider struct {
	name string
	// roleMappings maps custom role names to GCP IAM roles
	roleMappings map[string][]string
	// addedBindings tracks all role bindings that have been added
	// Key: "projectID:email:role"
	addedBindings map[string]bool
	// removedBindings tracks all role bindings that have been removed
	removedBindings map[string]bool
	// addPermissionCalls tracks all AddPermission calls
	addPermissionCalls []provider.PermissionRequest
	// removePermissionCalls tracks all RemovePermission calls
	removePermissionCalls []provider.PermissionRequest
	mu                    sync.Mutex
	shouldFail            bool
}

func newGCPSyncMockProvider() *gcpSyncMockProvider {
	return &gcpSyncMockProvider{
		name:                  string(provider.ProviderGCP),
		roleMappings:          make(map[string][]string),
		addedBindings:         make(map[string]bool),
		removedBindings:       make(map[string]bool),
		addPermissionCalls:    make([]provider.PermissionRequest, 0),
		removePermissionCalls: make([]provider.PermissionRequest, 0),
	}
}

func (m *gcpSyncMockProvider) Name() string {
	return m.name
}

func (m *gcpSyncMockProvider) AddPermission(ctx context.Context, req *provider.PermissionRequest) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if req != nil {
		m.addPermissionCalls = append(m.addPermissionCalls, *req)
		// Track each binding
		for _, ps := range req.PermissionSets {
			key := req.AccountID + ":" + req.UserEmail + ":" + ps
			m.addedBindings[key] = true
			delete(m.removedBindings, key)
		}
	}
	if m.shouldFail {
		return context.DeadlineExceeded
	}
	return nil
}

func (m *gcpSyncMockProvider) RemovePermission(ctx context.Context, req *provider.PermissionRequest) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if req != nil {
		m.removePermissionCalls = append(m.removePermissionCalls, *req)
		// Track each binding removal
		for _, ps := range req.PermissionSets {
			key := req.AccountID + ":" + req.UserEmail + ":" + ps
			m.removedBindings[key] = true
			delete(m.addedBindings, key)
		}
	}
	if m.shouldFail {
		return context.DeadlineExceeded
	}
	return nil
}

func (m *gcpSyncMockProvider) GetUserID(ctx context.Context, email string) (string, error) {
	return email, nil // GCP uses email as user ID
}

func (m *gcpSyncMockProvider) ValidateCredentials(ctx context.Context) error {
	return nil
}

func (m *gcpSyncMockProvider) Close() error {
	return nil
}

// SetRoleMappings sets the role mappings for the mock provider
func (m *gcpSyncMockProvider) SetRoleMappings(mappings map[string][]string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.roleMappings = make(map[string][]string)
	for k, v := range mappings {
		roles := make([]string, len(v))
		copy(roles, v)
		m.roleMappings[k] = roles
	}
}

// GetRoleMappings returns a copy of the current role mappings
func (m *gcpSyncMockProvider) GetRoleMappings() map[string][]string {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make(map[string][]string)
	for k, v := range m.roleMappings {
		roles := make([]string, len(v))
		copy(roles, v)
		result[k] = roles
	}
	return result
}

// HasBinding checks if a specific binding exists
func (m *gcpSyncMockProvider) HasBinding(projectID, email, role string) bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	key := projectID + ":" + email + ":" + role
	return m.addedBindings[key]
}

// WasBindingRemoved checks if a specific binding was removed
func (m *gcpSyncMockProvider) WasBindingRemoved(projectID, email, role string) bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	key := projectID + ":" + email + ":" + role
	return m.removedBindings[key]
}

// GetAddPermissionCalls returns a copy of all AddPermission calls
func (m *gcpSyncMockProvider) GetAddPermissionCalls() []provider.PermissionRequest {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]provider.PermissionRequest, len(m.addPermissionCalls))
	copy(result, m.addPermissionCalls)
	return result
}

// GetRemovePermissionCalls returns a copy of all RemovePermission calls
func (m *gcpSyncMockProvider) GetRemovePermissionCalls() []provider.PermissionRequest {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]provider.PermissionRequest, len(m.removePermissionCalls))
	copy(result, m.removePermissionCalls)
	return result
}

// Reset clears all tracked operations
func (m *gcpSyncMockProvider) Reset() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.addedBindings = make(map[string]bool)
	m.removedBindings = make(map[string]bool)
	m.addPermissionCalls = make([]provider.PermissionRequest, 0)
	m.removePermissionCalls = make([]provider.PermissionRequest, 0)
	m.shouldFail = false
}

// =============================================================================
// GCP Role Mapping Sync Manager
// =============================================================================

// GCPRoleMappingSyncManager handles synchronization of GCP permissions when
// role mappings change. This simulates the behavior described in Requirement 10.4.
type GCPRoleMappingSyncManager struct {
	provider   *gcpSyncMockProvider
	stateStore *gcpSyncMockStateStore
	config     *config.Config
	logger     *logger.Logger

	// currentMappings holds the current role mappings
	currentMappings map[string][]string
	mu              sync.Mutex
}

// NewGCPRoleMappingSyncManager creates a new GCP role mapping sync manager.
func NewGCPRoleMappingSyncManager(
	prov *gcpSyncMockProvider,
	store *gcpSyncMockStateStore,
	cfg *config.Config,
	log *logger.Logger,
) *GCPRoleMappingSyncManager {
	return &GCPRoleMappingSyncManager{
		provider:        prov,
		stateStore:      store,
		config:          cfg,
		logger:          log.WithModule("gcp-sync"),
		currentMappings: make(map[string][]string),
	}
}

// SetInitialMappings sets the initial role mappings
func (m *GCPRoleMappingSyncManager) SetInitialMappings(mappings map[string][]string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.currentMappings = make(map[string][]string)
	for k, v := range mappings {
		roles := make([]string, len(v))
		copy(roles, v)
		m.currentMappings[k] = roles
	}
	m.provider.SetRoleMappings(mappings)
}

// DetectChanges compares new mappings with current mappings and returns the changes.
// Returns: added mappings, removed mappings, modified mappings
func (m *GCPRoleMappingSyncManager) DetectChanges(newMappings map[string][]string) (
	added map[string][]string,
	removed map[string][]string,
	modified map[string]struct{ OldRoles, NewRoles []string },
) {
	m.mu.Lock()
	defer m.mu.Unlock()

	added = make(map[string][]string)
	removed = make(map[string][]string)
	modified = make(map[string]struct{ OldRoles, NewRoles []string })

	// Find added and modified mappings
	for roleName, newRoles := range newMappings {
		if oldRoles, exists := m.currentMappings[roleName]; exists {
			// Check if roles changed
			if !rolesEqual(oldRoles, newRoles) {
				modified[roleName] = struct{ OldRoles, NewRoles []string }{
					OldRoles: oldRoles,
					NewRoles: newRoles,
				}
			}
		} else {
			// New mapping
			added[roleName] = newRoles
		}
	}

	// Find removed mappings
	for roleName, oldRoles := range m.currentMappings {
		if _, exists := newMappings[roleName]; !exists {
			removed[roleName] = oldRoles
		}
	}

	return added, removed, modified
}

// SyncMappingChanges synchronizes permissions based on role mapping changes.
// This is the core function that implements Requirement 10.4.
func (m *GCPRoleMappingSyncManager) SyncMappingChanges(ctx context.Context, newMappings map[string][]string) error {
	_, removed, modified := m.DetectChanges(newMappings)

	// Get all active GCP instances
	filter := &state.InstanceFilter{
		CloudType: string(provider.ProviderGCP),
		Status:    state.StatusApproved,
	}
	instances, err := m.stateStore.ListInstances(ctx, filter)
	if err != nil {
		return err
	}

	// Process removed mappings - revoke permissions from affected users
	for roleName, oldRoles := range removed {
		affectedUsers := m.findUsersWithRole(instances, roleName)
		for _, user := range affectedUsers {
			for _, role := range oldRoles {
				req := &provider.PermissionRequest{
					AccountID:      user.AccountID,
					UserEmail:      user.Email,
					PermissionSets: []string{role},
				}
				if err := m.provider.RemovePermission(ctx, req); err != nil {
					m.logger.Error("Failed to remove permission for removed mapping",
						logger.UserEmail(user.Email),
						logger.String("role", role),
						logger.Err(err),
					)
				}
			}
		}
	}

	// Process modified mappings - update permissions for affected users
	for roleName, change := range modified {
		affectedUsers := m.findUsersWithRole(instances, roleName)
		for _, user := range affectedUsers {
			// Remove old roles
			for _, oldRole := range change.OldRoles {
				if !containsRole(change.NewRoles, oldRole) {
					req := &provider.PermissionRequest{
						AccountID:      user.AccountID,
						UserEmail:      user.Email,
						PermissionSets: []string{oldRole},
					}
					if err := m.provider.RemovePermission(ctx, req); err != nil {
						m.logger.Error("Failed to remove old role",
							logger.UserEmail(user.Email),
							logger.String("role", oldRole),
							logger.Err(err),
						)
					}
				}
			}
			// Add new roles
			for _, newRole := range change.NewRoles {
				if !containsRole(change.OldRoles, newRole) {
					req := &provider.PermissionRequest{
						AccountID:      user.AccountID,
						UserEmail:      user.Email,
						PermissionSets: []string{newRole},
					}
					if err := m.provider.AddPermission(ctx, req); err != nil {
						m.logger.Error("Failed to add new role",
							logger.UserEmail(user.Email),
							logger.String("role", newRole),
							logger.Err(err),
						)
					}
				}
			}
		}
	}

	// Update current mappings
	m.mu.Lock()
	m.currentMappings = make(map[string][]string)
	for k, v := range newMappings {
		roles := make([]string, len(v))
		copy(roles, v)
		m.currentMappings[k] = roles
	}
	m.mu.Unlock()
	m.provider.SetRoleMappings(newMappings)

	return nil
}

// findUsersWithRole finds all users who have a specific custom role
func (m *GCPRoleMappingSyncManager) findUsersWithRole(instances []*state.ApprovalInstance, roleName string) []*state.ApprovalInstance {
	var result []*state.ApprovalInstance
	for _, inst := range instances {
		for _, ps := range inst.PermissionSets {
			if ps == roleName {
				result = append(result, inst)
				break
			}
		}
	}
	return result
}

// GetCurrentMappings returns a copy of the current mappings
func (m *GCPRoleMappingSyncManager) GetCurrentMappings() map[string][]string {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make(map[string][]string)
	for k, v := range m.currentMappings {
		roles := make([]string, len(v))
		copy(roles, v)
		result[k] = roles
	}
	return result
}

// Helper functions
func rolesEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	aMap := make(map[string]bool)
	for _, r := range a {
		aMap[r] = true
	}
	for _, r := range b {
		if !aMap[r] {
			return false
		}
	}
	return true
}

func containsRole(roles []string, role string) bool {
	for _, r := range roles {
		if r == role {
			return true
		}
	}
	return false
}

// =============================================================================
// Generator Functions for GCP Sync Property-Based Testing
// =============================================================================

// genGCPEmail generates a valid email address for GCP sync testing.
func genGCPEmail() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		user := rapid.StringMatching(`[a-z][a-z0-9]{2,10}`).Draw(t, "user")
		domain := rapid.SampledFrom([]string{"example.com", "test.org", "company.io", "pingcap.com"}).Draw(t, "domain")
		return user + "@" + domain
	})
}

// genGCPProjectID generates a valid GCP project ID.
func genGCPProjectID() *rapid.Generator[string] {
	return rapid.StringMatching(`[a-z][a-z0-9-]{5,20}`)
}

// genGCPCustomRoleName generates a valid custom role name.
func genGCPCustomRoleName() *rapid.Generator[string] {
	return rapid.OneOf(
		rapid.SampledFrom([]string{
			"ViewOnly",
			"OnCall",
			"Admin",
			"Developer",
			"SecurityAdmin",
			"DataAnalyst",
			"NetworkAdmin",
		}),
		rapid.StringMatching(`[A-Z][a-zA-Z]{3,15}`),
	)
}

// genGCPIAMRole generates a valid GCP IAM role.
func genGCPIAMRole() *rapid.Generator[string] {
	return rapid.OneOf(
		rapid.SampledFrom([]string{
			"roles/viewer",
			"roles/editor",
			"roles/owner",
			"roles/compute.viewer",
			"roles/compute.admin",
			"roles/storage.objectViewer",
			"roles/storage.admin",
			"roles/bigquery.dataViewer",
			"roles/bigquery.admin",
			"roles/iam.securityAdmin",
		}),
		rapid.Custom(func(t *rapid.T) string {
			service := rapid.SampledFrom([]string{"compute", "storage", "bigquery", "iam", "logging"}).Draw(t, "service")
			role := rapid.SampledFrom([]string{"viewer", "admin", "editor", "user"}).Draw(t, "role")
			return "roles/" + service + "." + role
		}),
	)
}

// genGCPRoleMapping generates a role mapping (custom role name -> GCP IAM roles).
func genGCPRoleMapping() *rapid.Generator[map[string][]string] {
	return rapid.Custom(func(t *rapid.T) map[string][]string {
		numMappings := rapid.IntRange(1, 5).Draw(t, "num_mappings")
		mappings := make(map[string][]string)
		for i := 0; i < numMappings; i++ {
			roleName := genGCPCustomRoleName().Draw(t, "role_name")
			numRoles := rapid.IntRange(1, 4).Draw(t, "num_roles")
			roles := make([]string, numRoles)
			for j := 0; j < numRoles; j++ {
				roles[j] = genGCPIAMRole().Draw(t, "iam_role")
			}
			mappings[roleName] = roles
		}
		return mappings
	})
}

// genGCPApprovalInstance generates a GCP approval instance for testing.
func genGCPApprovalInstance(projectID string, customRoles []string) *rapid.Generator[*state.ApprovalInstance] {
	return rapid.Custom(func(t *rapid.T) *state.ApprovalInstance {
		startTime := time.Now().Add(-24 * time.Hour)
		endTime := time.Now().Add(time.Duration(rapid.IntRange(1, 720).Draw(t, "hours")) * time.Hour)
		instanceID := rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(t, "instance_id")

		// Select 1-3 custom roles from the available roles
		numRoles := rapid.IntRange(1, min(3, len(customRoles))).Draw(t, "num_roles")
		selectedRoles := make([]string, numRoles)
		// Randomly select roles from available roles
		for i := 0; i < numRoles; i++ {
			idx := rapid.IntRange(0, len(customRoles)-1).Draw(t, "role_idx")
			selectedRoles[i] = customRoles[idx]
		}

		return &state.ApprovalInstance{
			ID:             instanceID,
			Email:          genGCPEmail().Draw(t, "email"),
			AccountID:      projectID,
			PermissionSets: selectedRoles,
			StartTime:      startTime,
			EndTime:        endTime,
			Status:         state.StatusApproved,
			CloudType:      string(provider.ProviderGCP),
			CreatedAt:      startTime,
			UpdatedAt:      time.Now(),
		}
	})
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// =============================================================================
// GCP Sync Integration Test Setup
// =============================================================================

// gcpSyncTestSetup holds all components needed for GCP sync integration testing.
type gcpSyncTestSetup struct {
	syncManager *GCPRoleMappingSyncManager
	provider    *gcpSyncMockProvider
	stateStore  *gcpSyncMockStateStore
	config      *config.Config
	logger      *logger.Logger
}

// createGCPSyncTestSetup creates a complete test setup for GCP sync testing.
func createGCPSyncTestSetup(projectIDs []string) (*gcpSyncTestSetup, error) {
	// Create config with GCP projects
	cfg := &config.Config{
		Providers: config.ProvidersConfig{
			GCP: config.GCPConfig{
				Projects: make([]config.CloudAccount, len(projectIDs)),
			},
		},
		Runtime: config.RuntimeConfig{
			GracePeriodDays: 3,
			HTTPTimeout:     10 * time.Second,
			MaxRetries:      3,
		},
	}

	for i, projectID := range projectIDs {
		cfg.Providers.GCP.Projects[i] = config.CloudAccount{
			ID:   projectID,
			Name: "test-" + projectID,
			Type: "prod",
		}
	}

	// Create logger
	log := logger.NewLogger(logger.Config{Level: logger.ErrorLevel})

	// Create state store
	stateStore := newGCPSyncMockStateStore()

	// Create GCP provider
	gcpProvider := newGCPSyncMockProvider()

	// Create sync manager
	syncManager := NewGCPRoleMappingSyncManager(gcpProvider, stateStore, cfg, log)

	return &gcpSyncTestSetup{
		syncManager: syncManager,
		provider:    gcpProvider,
		stateStore:  stateStore,
		config:      cfg,
		logger:      log,
	}, nil
}

// =============================================================================
// Property 20: GCP 权限映射同步 - Integration Tests
// =============================================================================

// TestGCPSyncIntegration_RoleMappingChangeDetection tests that role mapping
// changes are correctly detected.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_RoleMappingChangeDetection(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Generate initial mappings
		initialMappings := genGCPRoleMapping().Draw(rt, "initial_mappings")
		setup.syncManager.SetInitialMappings(initialMappings)

		// Generate new mappings with some changes
		newMappings := genGCPRoleMapping().Draw(rt, "new_mappings")

		// Detect changes
		added, removed, modified := setup.syncManager.DetectChanges(newMappings)

		// Verify: All new mappings not in initial are detected as added
		for roleName := range newMappings {
			if _, existsInInitial := initialMappings[roleName]; !existsInInitial {
				if _, isAdded := added[roleName]; !isAdded {
					rt.Fatalf("New mapping %q should be detected as added", roleName)
				}
			}
		}

		// Verify: All initial mappings not in new are detected as removed
		for roleName := range initialMappings {
			if _, existsInNew := newMappings[roleName]; !existsInNew {
				if _, isRemoved := removed[roleName]; !isRemoved {
					rt.Fatalf("Old mapping %q should be detected as removed", roleName)
				}
			}
		}

		// Verify: Mappings in both with different roles are detected as modified
		for roleName, newRoles := range newMappings {
			if oldRoles, existsInInitial := initialMappings[roleName]; existsInInitial {
				if !rolesEqual(oldRoles, newRoles) {
					if _, isModified := modified[roleName]; !isModified {
						rt.Fatalf("Changed mapping %q should be detected as modified", roleName)
					}
				}
			}
		}
	})
}

// TestGCPSyncIntegration_NewRoleMappingsAppliedToExistingUsers tests that
// when new role mappings are added, they don't affect existing users
// (since users need to explicitly request the new role).
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_NewRoleMappingsAppliedToExistingUsers(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
			"Admin":    {"roles/editor", "roles/owner"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		// Create existing users with the initial roles
		ctx := context.Background()
		numUsers := rapid.IntRange(1, 5).Draw(rt, "num_users")
		for i := 0; i < numUsers; i++ {
			instance := &state.ApprovalInstance{
				ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "instance_id"),
				Email:          genGCPEmail().Draw(rt, "email"),
				AccountID:      projectID,
				PermissionSets: []string{"ViewOnly"},
				StartTime:      time.Now().Add(-24 * time.Hour),
				EndTime:        time.Now().Add(24 * time.Hour),
				Status:         state.StatusApproved,
				CloudType:      string(provider.ProviderGCP),
				CreatedAt:      time.Now().Add(-24 * time.Hour),
				UpdatedAt:      time.Now(),
			}
			if err := setup.stateStore.SaveInstance(ctx, instance); err != nil {
				rt.Fatalf("Failed to save instance: %v", err)
			}
		}

		// Add a new role mapping
		newMappings := map[string][]string{
			"ViewOnly":  {"roles/viewer"},
			"Admin":     {"roles/editor", "roles/owner"},
			"Developer": {"roles/compute.admin", "roles/storage.admin"}, // New mapping
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: No permission changes for existing users (they don't have the new role)
		addCalls := setup.provider.GetAddPermissionCalls()
		removeCalls := setup.provider.GetRemovePermissionCalls()

		// Since no existing users have "Developer" role, no changes should be made
		if len(addCalls) != 0 || len(removeCalls) != 0 {
			rt.Fatalf("Expected no permission changes for new role mapping, got %d adds and %d removes",
				len(addCalls), len(removeCalls))
		}

		// Verify: New mapping is now in current mappings
		currentMappings := setup.syncManager.GetCurrentMappings()
		if _, exists := currentMappings["Developer"]; !exists {
			rt.Fatal("New mapping 'Developer' should be in current mappings")
		}
	})
}

// TestGCPSyncIntegration_RemovedRoleMappingsRevokePermissions tests that
// when role mappings are removed, affected users' permissions are revoked.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_RemovedRoleMappingsRevokePermissions(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings with a role that will be removed
		initialMappings := map[string][]string{
			"ViewOnly":  {"roles/viewer"},
			"Admin":     {"roles/editor", "roles/owner"},
			"Developer": {"roles/compute.admin"}, // This will be removed
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		// Create users with the Developer role
		ctx := context.Background()
		numUsers := rapid.IntRange(1, 5).Draw(rt, "num_users")
		affectedEmails := make([]string, numUsers)
		for i := 0; i < numUsers; i++ {
			email := genGCPEmail().Draw(rt, "email")
			affectedEmails[i] = email
			instance := &state.ApprovalInstance{
				ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "instance_id"),
				Email:          email,
				AccountID:      projectID,
				PermissionSets: []string{"Developer"},
				StartTime:      time.Now().Add(-24 * time.Hour),
				EndTime:        time.Now().Add(24 * time.Hour),
				Status:         state.StatusApproved,
				CloudType:      string(provider.ProviderGCP),
				CreatedAt:      time.Now().Add(-24 * time.Hour),
				UpdatedAt:      time.Now(),
			}
			if err := setup.stateStore.SaveInstance(ctx, instance); err != nil {
				rt.Fatalf("Failed to save instance: %v", err)
			}
		}

		// Remove the Developer role mapping
		newMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
			"Admin":    {"roles/editor", "roles/owner"},
			// Developer is removed
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: RemovePermission was called for all affected users
		removeCalls := setup.provider.GetRemovePermissionCalls()
		if len(removeCalls) != numUsers {
			rt.Fatalf("Expected %d remove calls, got %d", numUsers, len(removeCalls))
		}

		// Verify: Each affected user had their permission removed
		for _, email := range affectedEmails {
			if !setup.provider.WasBindingRemoved(projectID, email, "roles/compute.admin") {
				rt.Fatalf("Permission should have been removed for user %s", email)
			}
		}
	})
}

// TestGCPSyncIntegration_ModifiedRoleMappingsUpdatePermissions tests that
// when role mappings are modified, affected users' permissions are updated.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_ModifiedRoleMappingsUpdatePermissions(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
			"Admin":    {"roles/editor", "roles/owner"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		// Create users with the Admin role
		ctx := context.Background()
		numUsers := rapid.IntRange(1, 5).Draw(rt, "num_users")
		affectedEmails := make([]string, numUsers)
		for i := 0; i < numUsers; i++ {
			email := genGCPEmail().Draw(rt, "email")
			affectedEmails[i] = email
			instance := &state.ApprovalInstance{
				ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "instance_id"),
				Email:          email,
				AccountID:      projectID,
				PermissionSets: []string{"Admin"},
				StartTime:      time.Now().Add(-24 * time.Hour),
				EndTime:        time.Now().Add(24 * time.Hour),
				Status:         state.StatusApproved,
				CloudType:      string(provider.ProviderGCP),
				CreatedAt:      time.Now().Add(-24 * time.Hour),
				UpdatedAt:      time.Now(),
			}
			if err := setup.stateStore.SaveInstance(ctx, instance); err != nil {
				rt.Fatalf("Failed to save instance: %v", err)
			}
		}

		// Modify the Admin role mapping - remove "roles/owner", add "roles/iam.securityAdmin"
		newMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
			"Admin":    {"roles/editor", "roles/iam.securityAdmin"}, // Modified
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: RemovePermission was called for removed role (roles/owner)
		// and AddPermission was called for new role (roles/iam.securityAdmin)
		removeCalls := setup.provider.GetRemovePermissionCalls()
		addCalls := setup.provider.GetAddPermissionCalls()

		// Each user should have 1 remove call (roles/owner) and 1 add call (roles/iam.securityAdmin)
		if len(removeCalls) != numUsers {
			rt.Fatalf("Expected %d remove calls, got %d", numUsers, len(removeCalls))
		}
		if len(addCalls) != numUsers {
			rt.Fatalf("Expected %d add calls, got %d", numUsers, len(addCalls))
		}

		// Verify: Each affected user had the correct permission changes
		for _, email := range affectedEmails {
			if !setup.provider.WasBindingRemoved(projectID, email, "roles/owner") {
				rt.Fatalf("Old role should have been removed for user %s", email)
			}
			if !setup.provider.HasBinding(projectID, email, "roles/iam.securityAdmin") {
				rt.Fatalf("New role should have been added for user %s", email)
			}
		}
	})
}

// TestGCPSyncIntegration_MultipleUsersWithSameRoleAllUpdated tests that
// when a role mapping changes, all users with that role are updated.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_MultipleUsersWithSameRoleAllUpdated(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		// Create multiple users with the same ViewOnly role
		ctx := context.Background()
		numUsers := rapid.IntRange(2, 10).Draw(rt, "num_users")
		affectedEmails := make([]string, numUsers)
		for i := 0; i < numUsers; i++ {
			email := genGCPEmail().Draw(rt, "email")
			affectedEmails[i] = email
			instance := &state.ApprovalInstance{
				ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "instance_id"),
				Email:          email,
				AccountID:      projectID,
				PermissionSets: []string{"ViewOnly"},
				StartTime:      time.Now().Add(-24 * time.Hour),
				EndTime:        time.Now().Add(24 * time.Hour),
				Status:         state.StatusApproved,
				CloudType:      string(provider.ProviderGCP),
				CreatedAt:      time.Now().Add(-24 * time.Hour),
				UpdatedAt:      time.Now(),
			}
			if err := setup.stateStore.SaveInstance(ctx, instance); err != nil {
				rt.Fatalf("Failed to save instance: %v", err)
			}
		}

		// Modify the ViewOnly role mapping - add additional role
		newMappings := map[string][]string{
			"ViewOnly": {"roles/viewer", "roles/logging.viewer"}, // Added roles/logging.viewer
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: AddPermission was called for all users
		addCalls := setup.provider.GetAddPermissionCalls()
		if len(addCalls) != numUsers {
			rt.Fatalf("Expected %d add calls (one per user), got %d", numUsers, len(addCalls))
		}

		// Verify: Each user got the new role
		for _, email := range affectedEmails {
			if !setup.provider.HasBinding(projectID, email, "roles/logging.viewer") {
				rt.Fatalf("New role should have been added for user %s", email)
			}
		}
	})
}

// TestGCPSyncIntegration_OnlyGCPUsersAffected tests that role mapping changes
// only affect GCP users, not users from other cloud platforms.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_OnlyGCPUsersAffected(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		ctx := context.Background()

		// Create a GCP user with ViewOnly role
		gcpEmail := genGCPEmail().Draw(rt, "gcp_email")
		gcpInstance := &state.ApprovalInstance{
			ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "gcp_instance_id"),
			Email:          gcpEmail,
			AccountID:      projectID,
			PermissionSets: []string{"ViewOnly"},
			StartTime:      time.Now().Add(-24 * time.Hour),
			EndTime:        time.Now().Add(24 * time.Hour),
			Status:         state.StatusApproved,
			CloudType:      string(provider.ProviderGCP),
			CreatedAt:      time.Now().Add(-24 * time.Hour),
			UpdatedAt:      time.Now(),
		}
		if err := setup.stateStore.SaveInstance(ctx, gcpInstance); err != nil {
			rt.Fatalf("Failed to save GCP instance: %v", err)
		}

		// Create an AWS user with the same role name (should not be affected)
		awsEmail := genGCPEmail().Draw(rt, "aws_email")
		awsInstance := &state.ApprovalInstance{
			ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "aws_instance_id"),
			Email:          awsEmail,
			AccountID:      "123456789012", // AWS account ID
			PermissionSets: []string{"ViewOnly"},
			StartTime:      time.Now().Add(-24 * time.Hour),
			EndTime:        time.Now().Add(24 * time.Hour),
			Status:         state.StatusApproved,
			CloudType:      string(provider.ProviderAWS), // AWS, not GCP
			CreatedAt:      time.Now().Add(-24 * time.Hour),
			UpdatedAt:      time.Now(),
		}
		if err := setup.stateStore.SaveInstance(ctx, awsInstance); err != nil {
			rt.Fatalf("Failed to save AWS instance: %v", err)
		}

		// Modify the ViewOnly role mapping
		newMappings := map[string][]string{
			"ViewOnly": {"roles/viewer", "roles/logging.viewer"},
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: Only GCP user was affected
		addCalls := setup.provider.GetAddPermissionCalls()
		if len(addCalls) != 1 {
			rt.Fatalf("Expected 1 add call (only GCP user), got %d", len(addCalls))
		}

		// Verify: The call was for the GCP user
		if addCalls[0].UserEmail != gcpEmail {
			rt.Fatalf("Expected add call for GCP user %s, got %s", gcpEmail, addCalls[0].UserEmail)
		}
	})
}

// TestGCPSyncIntegration_OnlyActiveUsersAffected tests that role mapping changes
// only affect users with approved (active) status.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_OnlyActiveUsersAffected(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		ctx := context.Background()

		// Create an active (approved) user
		activeEmail := genGCPEmail().Draw(rt, "active_email")
		activeInstance := &state.ApprovalInstance{
			ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "active_instance_id"),
			Email:          activeEmail,
			AccountID:      projectID,
			PermissionSets: []string{"ViewOnly"},
			StartTime:      time.Now().Add(-24 * time.Hour),
			EndTime:        time.Now().Add(24 * time.Hour),
			Status:         state.StatusApproved, // Active
			CloudType:      string(provider.ProviderGCP),
			CreatedAt:      time.Now().Add(-24 * time.Hour),
			UpdatedAt:      time.Now(),
		}
		if err := setup.stateStore.SaveInstance(ctx, activeInstance); err != nil {
			rt.Fatalf("Failed to save active instance: %v", err)
		}

		// Create an expired user (should not be affected)
		expiredEmail := genGCPEmail().Draw(rt, "expired_email")
		expiredInstance := &state.ApprovalInstance{
			ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "expired_instance_id"),
			Email:          expiredEmail,
			AccountID:      projectID,
			PermissionSets: []string{"ViewOnly"},
			StartTime:      time.Now().Add(-48 * time.Hour),
			EndTime:        time.Now().Add(-24 * time.Hour),
			Status:         state.StatusExpired, // Expired
			CloudType:      string(provider.ProviderGCP),
			CreatedAt:      time.Now().Add(-48 * time.Hour),
			UpdatedAt:      time.Now(),
		}
		if err := setup.stateStore.SaveInstance(ctx, expiredInstance); err != nil {
			rt.Fatalf("Failed to save expired instance: %v", err)
		}

		// Modify the ViewOnly role mapping
		newMappings := map[string][]string{
			"ViewOnly": {"roles/viewer", "roles/logging.viewer"},
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: Only active user was affected
		addCalls := setup.provider.GetAddPermissionCalls()
		if len(addCalls) != 1 {
			rt.Fatalf("Expected 1 add call (only active user), got %d", len(addCalls))
		}

		// Verify: The call was for the active user
		if addCalls[0].UserEmail != activeEmail {
			rt.Fatalf("Expected add call for active user %s, got %s", activeEmail, addCalls[0].UserEmail)
		}
	})
}

// TestGCPSyncIntegration_NoChangesNoOperations tests that when there are no
// changes to role mappings, no permission operations are performed.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_NoChangesNoOperations(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := genGCPRoleMapping().Draw(rt, "initial_mappings")
		setup.syncManager.SetInitialMappings(initialMappings)

		// Create users with the roles
		ctx := context.Background()
		roleNames := make([]string, 0, len(initialMappings))
		for roleName := range initialMappings {
			roleNames = append(roleNames, roleName)
		}

		if len(roleNames) > 0 {
			numUsers := rapid.IntRange(1, 5).Draw(rt, "num_users")
			for i := 0; i < numUsers; i++ {
				roleName := roleNames[rapid.IntRange(0, len(roleNames)-1).Draw(rt, "role_index")]
				instance := &state.ApprovalInstance{
					ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "instance_id"),
					Email:          genGCPEmail().Draw(rt, "email"),
					AccountID:      projectID,
					PermissionSets: []string{roleName},
					StartTime:      time.Now().Add(-24 * time.Hour),
					EndTime:        time.Now().Add(24 * time.Hour),
					Status:         state.StatusApproved,
					CloudType:      string(provider.ProviderGCP),
					CreatedAt:      time.Now().Add(-24 * time.Hour),
					UpdatedAt:      time.Now(),
				}
				if err := setup.stateStore.SaveInstance(ctx, instance); err != nil {
					rt.Fatalf("Failed to save instance: %v", err)
				}
			}
		}

		// Sync with the same mappings (no changes)
		err = setup.syncManager.SyncMappingChanges(ctx, initialMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: No permission operations were performed
		addCalls := setup.provider.GetAddPermissionCalls()
		removeCalls := setup.provider.GetRemovePermissionCalls()

		if len(addCalls) != 0 {
			rt.Fatalf("Expected 0 add calls when no changes, got %d", len(addCalls))
		}
		if len(removeCalls) != 0 {
			rt.Fatalf("Expected 0 remove calls when no changes, got %d", len(removeCalls))
		}
	})
}

// TestGCPSyncIntegration_MultipleProjectsHandledCorrectly tests that role mapping
// changes are applied correctly across multiple GCP projects.
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_MultipleProjectsHandledCorrectly(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		// Generate multiple project IDs
		numProjects := rapid.IntRange(2, 4).Draw(rt, "num_projects")
		projectIDs := make([]string, numProjects)
		for i := 0; i < numProjects; i++ {
			projectIDs[i] = genGCPProjectID().Draw(rt, "project_id")
		}

		setup, err := createGCPSyncTestSetup(projectIDs)
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings
		initialMappings := map[string][]string{
			"ViewOnly": {"roles/viewer"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		ctx := context.Background()

		// Create users in different projects with the same role
		usersPerProject := make(map[string][]string)
		for _, projectID := range projectIDs {
			numUsers := rapid.IntRange(1, 3).Draw(rt, "num_users")
			emails := make([]string, numUsers)
			for i := 0; i < numUsers; i++ {
				email := genGCPEmail().Draw(rt, "email")
				emails[i] = email
				instance := &state.ApprovalInstance{
					ID:             rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "instance_id"),
					Email:          email,
					AccountID:      projectID,
					PermissionSets: []string{"ViewOnly"},
					StartTime:      time.Now().Add(-24 * time.Hour),
					EndTime:        time.Now().Add(24 * time.Hour),
					Status:         state.StatusApproved,
					CloudType:      string(provider.ProviderGCP),
					CreatedAt:      time.Now().Add(-24 * time.Hour),
					UpdatedAt:      time.Now(),
				}
				if err := setup.stateStore.SaveInstance(ctx, instance); err != nil {
					rt.Fatalf("Failed to save instance: %v", err)
				}
			}
			usersPerProject[projectID] = emails
		}

		// Modify the ViewOnly role mapping
		newMappings := map[string][]string{
			"ViewOnly": {"roles/viewer", "roles/logging.viewer"},
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: All users in all projects were updated
		for projectID, emails := range usersPerProject {
			for _, email := range emails {
				if !setup.provider.HasBinding(projectID, email, "roles/logging.viewer") {
					rt.Fatalf("New role should have been added for user %s in project %s", email, projectID)
				}
			}
		}
	})
}

// TestGCPSyncIntegration_ComplexMappingChanges tests complex scenarios with
// multiple simultaneous changes (additions, removals, and modifications).
//
// Feature: autosso-refactoring, Property 20: GCP 权限映射同步
// **Validates: Requirements 10.4**
func TestGCPSyncIntegration_ComplexMappingChanges(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		projectID := genGCPProjectID().Draw(rt, "project_id")

		setup, err := createGCPSyncTestSetup([]string{projectID})
		if err != nil {
			rt.Fatalf("Failed to create test setup: %v", err)
		}

		// Set up initial mappings with multiple roles
		initialMappings := map[string][]string{
			"ViewOnly":  {"roles/viewer"},
			"Admin":     {"roles/editor", "roles/owner"},
			"Developer": {"roles/compute.admin"},
			"ToRemove":  {"roles/storage.admin"},
		}
		setup.syncManager.SetInitialMappings(initialMappings)

		ctx := context.Background()

		// Create users with different roles
		viewOnlyEmail := genGCPEmail().Draw(rt, "viewonly_email")
		adminEmail := genGCPEmail().Draw(rt, "admin_email")
		developerEmail := genGCPEmail().Draw(rt, "developer_email")
		toRemoveEmail := genGCPEmail().Draw(rt, "toremove_email")

		instances := []*state.ApprovalInstance{
			{
				ID: rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "id1"),
				Email: viewOnlyEmail, AccountID: projectID, PermissionSets: []string{"ViewOnly"},
				StartTime: time.Now().Add(-24 * time.Hour), EndTime: time.Now().Add(24 * time.Hour),
				Status: state.StatusApproved, CloudType: string(provider.ProviderGCP),
				CreatedAt: time.Now().Add(-24 * time.Hour), UpdatedAt: time.Now(),
			},
			{
				ID: rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "id2"),
				Email: adminEmail, AccountID: projectID, PermissionSets: []string{"Admin"},
				StartTime: time.Now().Add(-24 * time.Hour), EndTime: time.Now().Add(24 * time.Hour),
				Status: state.StatusApproved, CloudType: string(provider.ProviderGCP),
				CreatedAt: time.Now().Add(-24 * time.Hour), UpdatedAt: time.Now(),
			},
			{
				ID: rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "id3"),
				Email: developerEmail, AccountID: projectID, PermissionSets: []string{"Developer"},
				StartTime: time.Now().Add(-24 * time.Hour), EndTime: time.Now().Add(24 * time.Hour),
				Status: state.StatusApproved, CloudType: string(provider.ProviderGCP),
				CreatedAt: time.Now().Add(-24 * time.Hour), UpdatedAt: time.Now(),
			},
			{
				ID: rapid.StringMatching(`[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}`).Draw(rt, "id4"),
				Email: toRemoveEmail, AccountID: projectID, PermissionSets: []string{"ToRemove"},
				StartTime: time.Now().Add(-24 * time.Hour), EndTime: time.Now().Add(24 * time.Hour),
				Status: state.StatusApproved, CloudType: string(provider.ProviderGCP),
				CreatedAt: time.Now().Add(-24 * time.Hour), UpdatedAt: time.Now(),
			},
		}

		for _, inst := range instances {
			if err := setup.stateStore.SaveInstance(ctx, inst); err != nil {
				rt.Fatalf("Failed to save instance: %v", err)
			}
		}

		// Apply complex changes:
		// - ViewOnly: unchanged
		// - Admin: modified (remove roles/owner, add roles/iam.securityAdmin)
		// - Developer: unchanged
		// - ToRemove: removed
		// - NewRole: added (but no users have it)
		newMappings := map[string][]string{
			"ViewOnly":  {"roles/viewer"},                              // Unchanged
			"Admin":     {"roles/editor", "roles/iam.securityAdmin"},   // Modified
			"Developer": {"roles/compute.admin"},                       // Unchanged
			"NewRole":   {"roles/bigquery.admin"},                      // Added
			// ToRemove is removed
		}

		// Sync the changes
		err = setup.syncManager.SyncMappingChanges(ctx, newMappings)
		if err != nil {
			rt.Fatalf("SyncMappingChanges failed: %v", err)
		}

		// Verify: ViewOnly user unchanged (no operations)
		// Verify: Admin user had roles/owner removed and roles/iam.securityAdmin added
		if !setup.provider.WasBindingRemoved(projectID, adminEmail, "roles/owner") {
			rt.Fatal("Admin user should have roles/owner removed")
		}
		if !setup.provider.HasBinding(projectID, adminEmail, "roles/iam.securityAdmin") {
			rt.Fatal("Admin user should have roles/iam.securityAdmin added")
		}

		// Verify: ToRemove user had roles/storage.admin removed
		if !setup.provider.WasBindingRemoved(projectID, toRemoveEmail, "roles/storage.admin") {
			rt.Fatal("ToRemove user should have roles/storage.admin removed")
		}
	})
}
