package permissions

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"math"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

func newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
	mutation AssignmentPolicyEligibilityMutation,
) cp.ComponentManagedPlannedTransactionOperationProvider {
	return NewPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		mutation,
		assignmentPolicyTestEligibilityProvider(),
	)
}

func newTestPermissionSetAssignmentPolicyEligibilityTransitionsProvider(
	mutations []AssignmentPolicyEligibilityMutation,
) cp.ComponentManagedPlannedTransactionOperationProvider {
	return NewPermissionSetAssignmentPolicyEligibilityTransitionsProvider(
		mutations,
		assignmentPolicyTestEligibilityProvider(),
	)
}

func TestAssignmentPolicyUserTransitionRevokesLossAndSynchronouslyGrantsGainInOneStage(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(context.Background(), cp.ComponentManagedScopeItem, item, plan, store); err != nil {
		t.Fatal(err)
	}

	loss := assignmentPolicyTestEligibilityMutation("user/user-sales", []string{"Sales"}, nil, false)
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(loss)
	lossPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	assertAssignmentPolicyManagedTarget(t, lossPlan, PermissionSetAssignmentPolicySerializationTarget())
	if len(lossPlan.Targets()) != 1 {
		t.Fatalf("loss targets = %#v", lossPlan.Targets())
	}
	store.rows["User"][0]["IsActive"] = false
	store.locks = nil
	result, err := provider.Mutate(context.Background(), lossPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if result.Disposition != cp.ComponentManagedMutationSafeProjectionStaged {
		t.Fatalf("loss disposition = %q", result.Disposition)
	}
	lockIndex := map[string]int{
		schemacatalog.AssignmentPolicySelectorTable:    -1,
		schemacatalog.AssignmentPolicyTargetStateTable: -1,
		"PermissionSetAssignment":                      -1,
	}
	for index, query := range store.locks {
		if query.Table == "Profile" || query.Table == "User" {
			t.Fatalf("loss transition acquired forbidden source lock after target serialization: %#v", query)
		}
		if _, tracked := lockIndex[query.Table]; tracked && lockIndex[query.Table] == -1 {
			lockIndex[query.Table] = index
		}
	}
	if lockIndex[schemacatalog.AssignmentPolicySelectorTable] == -1 ||
		lockIndex[schemacatalog.AssignmentPolicyTargetStateTable] == -1 ||
		lockIndex["PermissionSetAssignment"] == -1 ||
		lockIndex[schemacatalog.AssignmentPolicySelectorTable] > lockIndex["PermissionSetAssignment"] ||
		lockIndex[schemacatalog.AssignmentPolicyTargetStateTable] > lockIndex["PermissionSetAssignment"] {
		t.Fatalf("loss eligibility lock order = %#v; locks=%#v", lockIndex, store.locks)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row["IsDeleted"] != true {
		t.Fatalf("eligibility loss did not revoke in source transaction: %#v", row)
	}
	receipts := store.rows[schemacatalog.AssignmentPolicyTombstoneReceiptTable]
	if len(receipts) != 1 || receipts[0]["AssignmentId"] != assignmentPolicyTestAssignment(store, "user-sales")["Id"] ||
		receipts[0]["PermissionSetId"] != "ps-sales" || receipts[0]["AssigneeId"] != "user-sales" ||
		receipts[0]["OwnerPack"] != "app" || receipts[0]["PolicyKey"] != "sales_users" ||
		receipts[0]["TombstonedGeneration"] != uint64(2) ||
		receipts[0]["TombstonedAt"] != assignmentPolicyTestAssignment(store, "user-sales")["LastModifiedDate"] {
		t.Fatalf("eligibility-loss tombstone receipts = %#v", receipts)
	}
	projection, err := provider.Project(context.Background(), lossPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(projection.Expected, projection.Live) {
		t.Fatalf("loss projection differs:\nexpected=%s\nlive=%s", projection.Expected, projection.Live)
	}

	store.rows["User"] = append(store.rows["User"], cp.ComponentRow{
		"Id": "user-gain", "ProfileId": "profile-sales", "IsActive": true, "IsDeleted": false,
	})
	stateBefore := store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]["WorkGeneration"]
	gainProvider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-gain", nil, []string{"Sales"}, false),
	)
	gainPlan, err := gainProvider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	result, err = gainProvider.Mutate(context.Background(), gainPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if result.Disposition != cp.ComponentManagedMutationSafeProjectionStaged {
		t.Fatalf("gain disposition = %q", result.Disposition)
	}
	if row := assignmentPolicyTestAssignment(store, "user-gain"); row == nil || row["IsDeleted"] != false {
		t.Fatalf("eligibility gain was not granted synchronously: %#v", row)
	}
	if rows := store.rows[schemacatalog.AssignmentPolicyTombstoneReceiptTable]; len(rows) != 1 {
		t.Fatalf("eligibility gain changed tombstone receipts: %#v", rows)
	}
	state := store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]
	if state["WorkGeneration"] == stateBefore || state["WorkGeneration"] != state["AppliedGeneration"] {
		t.Fatalf("gain did not settle one synchronous generation: %#v", state)
	}
	projection, err = gainProvider.Project(context.Background(), gainPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(projection.Expected, projection.Live) {
		t.Fatalf("gain projection differs:\nexpected=%s\nlive=%s", projection.Expected, projection.Live)
	}
	// Project runs after transaction-bound effects. It must observe and reject
	// an effect that removes a newly granted row in the same transaction.
	assignmentPolicyTestAssignment(store, "user-gain")["IsDeleted"] = true
	projection, err = gainProvider.Project(context.Background(), gainPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Equal(projection.Expected, projection.Live) {
		t.Fatal("gain projection accepted a post-mutation missing assignment")
	}
}

func TestAssignmentPolicySourceTransitionDoesNotAcquireSourceLocksAfterTargetSerialization(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, plan, store,
	); err != nil {
		t.Fatal(err)
	}

	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-sales", []string{"Sales"}, nil, false),
	)
	sourcePlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	store.rows["User"][0]["IsActive"] = false
	guard := &assignmentPolicyOperationNoSourceLockStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
	}
	result, err := provider.Mutate(context.Background(), sourcePlan, guard)
	if err != nil {
		t.Fatalf("Mutate acquired a forbidden Profile/User lock after target serialization: %v", err)
	}
	if result.Disposition != cp.ComponentManagedMutationSafeProjectionStaged {
		t.Fatalf("disposition = %q, want %q", result.Disposition, cp.ComponentManagedMutationSafeProjectionStaged)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row["IsDeleted"] != true {
		t.Fatalf("transaction-visible eligibility loss was not revoked: %#v", row)
	}
}

func TestAssignmentPolicyRemovalFinalizesTombstoneAfterSynchronousGain(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	itemPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, itemPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	originalAssignmentID := assignmentPolicyTestAssignment(store, "user-sales")["Id"]

	lossProvider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-sales", []string{"Sales"}, nil, false),
	)
	lossPlan, err := lossProvider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	store.rows["User"][0]["IsActive"] = false
	if _, err := lossProvider.Mutate(context.Background(), lossPlan, store); err != nil {
		t.Fatal(err)
	}

	gainProvider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-sales", nil, []string{"Sales"}, false),
	)
	store.rows["User"][0]["IsActive"] = true
	gainPlan, err := gainProvider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := gainProvider.Mutate(context.Background(), gainPlan, store); err != nil {
		t.Fatal(err)
	}
	state := store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]
	if state["WorkGeneration"] != state["AppliedGeneration"] {
		t.Fatalf("gain did not settle synchronously: %#v", state)
	}
	if restored := assignmentPolicyTestAssignment(store, "user-sales"); restored == nil || restored["Id"] != originalAssignmentID || restored["IsDeleted"] != false {
		t.Fatalf("pre-release gain did not reactivate the suspension tombstone: %#v", restored)
	}

	setRequest, err := cp.NewComponentManagedSetLifecycleRequest(cp.ComponentManagedSetRequest{
		PackName: "app", Kind: PermissionSetAssignmentPolicyComponentKind,
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	setPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeSet, setRequest, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeSet, setRequest, setPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	if tombstones := store.rows[schemacatalog.AssignmentPolicyTombstoneReceiptTable]; len(tombstones) != 0 {
		t.Fatalf("release retained tombstone receipts: %#v", tombstones)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row != nil {
		t.Fatalf("release retained assignment tombstone: %#v", row)
	}

	reapplyPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, reapplyPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	restored := assignmentPolicyTestAssignment(store, "user-sales")
	if restored["Id"] == originalAssignmentID || restored["IsDeleted"] != false {
		t.Fatalf("reapplied assignment = %#v, want a new id after release (old %v)", restored, originalAssignmentID)
	}
}

func TestAssignmentPolicySourceTransitionBindsDefinitionOwnerBeforeMutation(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(context.Background(), cp.ComponentManagedScopeItem, item, plan, store); err != nil {
		t.Fatal(err)
	}

	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-sales", []string{"Sales"}, nil, false),
	)
	transitionPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := decodeAssignmentPolicySourcePlan(transitionPlan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.LossTargets) != 1 || decoded.LossTargets[0].DefinitionOwnerPack != "definitions" {
		t.Fatalf("source target owner binding = %#v", decoded.LossTargets)
	}
	store.rows["User"][0]["IsActive"] = false
	store.rows["PermissionSet"][0]["OwnerPack"] = "drifted-definitions"
	if _, err := provider.Mutate(context.Background(), transitionPlan, store); err == nil ||
		!strings.Contains(err.Error(), "identity") {
		t.Fatalf("Definition-owner drift error = %v", err)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row["IsDeleted"] != false {
		t.Fatalf("Definition-owner drift mutated assignment: %#v", row)
	}
}

func TestAssignmentPolicySourceTransitionRejectsExhaustedGeneration(t *testing.T) {
	for _, test := range []struct {
		name     string
		mutation AssignmentPolicyEligibilityMutation
		stage    func(*permissionSetDefinitionLifecycleMemoryStore)
	}{
		{
			name: "loss",
			mutation: assignmentPolicyTestEligibilityMutation(
				"user/user-sales", []string{"Sales"}, nil, false,
			),
			stage: func(store *permissionSetDefinitionLifecycleMemoryStore) {
				store.rows["User"][0]["IsActive"] = false
			},
		},
		{
			name: "gain",
			mutation: assignmentPolicyTestEligibilityMutation(
				"user/user-gain", nil, []string{"Sales"}, false,
			),
			stage: func(store *permissionSetDefinitionLifecycleMemoryStore) {
				store.rows["User"] = append(store.rows["User"], cp.ComponentRow{
					"Id": "user-gain", "ProfileId": "profile-sales", "IsActive": true, "IsDeleted": false,
				})
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
			if err != nil {
				t.Fatal(err)
			}
			store := assignmentPolicyLifecycleStore()
			item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
			plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
			if err != nil {
				t.Fatal(err)
			}
			if _, err := lifecycle.Lifecycle().Mutate(context.Background(), cp.ComponentManagedScopeItem, item, plan, store); err != nil {
				t.Fatal(err)
			}
			provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(test.mutation)
			transitionPlan, err := provider.Plan(context.Background(), store)
			if err != nil {
				t.Fatal(err)
			}
			state := store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]
			state["WorkGeneration"] = uint64(math.MaxUint64)
			state["AppliedGeneration"] = uint64(math.MaxUint64)
			test.stage(store)
			if _, err := provider.Mutate(context.Background(), transitionPlan, store); err == nil ||
				!strings.Contains(err.Error(), "generation") {
				t.Fatalf("exhausted generation error = %v", err)
			}
		})
	}
}

func TestAssignmentPolicySourceTransitionProjectionRejectsMissingStateWithoutPanic(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{{
		"ProfileName": "Sales", "PermissionSetId": "ps-sales",
	}}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-gain", nil, []string{"Sales"}, false),
	)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			t.Fatalf("Project panicked on missing target state: %v", recovered)
		}
	}()
	if _, err := provider.Project(context.Background(), plan, store); err == nil ||
		!strings.Contains(err.Error(), "state") {
		t.Fatalf("missing target state error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionRequiresExactStateUpdate(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(context.Background(), cp.ComponentManagedScopeItem, item, plan, store); err != nil {
		t.Fatal(err)
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("user/user-gain", nil, []string{"Sales"}, false),
	)
	transitionPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	store.rows["User"] = append(store.rows["User"], cp.ComponentRow{
		"Id": "user-gain", "ProfileId": "profile-sales", "IsActive": true, "IsDeleted": false,
	})
	zeroState := &assignmentPolicySourceZeroStateUpdateStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
	}
	if _, err := provider.Mutate(context.Background(), transitionPlan, zeroState); err == nil ||
		!strings.Contains(err.Error(), "affected 0") {
		t.Fatalf("zero-row durable state update error = %v", err)
	}
}

type assignmentPolicySourceZeroStateUpdateStore struct {
	*permissionSetDefinitionLifecycleMemoryStore
}

func (store *assignmentPolicySourceZeroStateUpdateStore) UpdateRows(
	ctx context.Context,
	update cp.ComponentManagedUpdate,
) (int64, error) {
	if update.Table == schemacatalog.AssignmentPolicyTargetStateTable {
		return 0, nil
	}
	return store.permissionSetDefinitionLifecycleMemoryStore.UpdateRows(ctx, update)
}

func TestAssignmentPolicySourceTransitionLocksOldAndNewPolicyPairsTogether(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows["Profile"] = append(store.rows["Profile"], cp.ComponentRow{
		"Id": "profile-standard", "Name": "Standard", "IsActive": true, "IsDeleted": false,
	})
	store.rows["PermissionSet"] = append(store.rows["PermissionSet"], cp.ComponentRow{
		"Id": "ps-standard", "Name": "Standard_User", "OwnerPack": "definitions",
		"IsActive": true, "IsDeleted": false, "IsCustom": true,
		"ProfileId": nil, "IsOwnedByProfile": false,
	})
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{
		{"ProfileName": "Sales", "PermissionSetId": "ps-sales"},
		{"ProfileName": "Standard", "PermissionSetId": "ps-standard"},
	}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{
		{"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users"},
		{"Id": "claim-standard", "PermissionSetId": "ps-standard", "OwnerPack": "app", "PolicyKey": "standard_users"},
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation(
			"user/user-sales", []string{"Sales"}, []string{"Standard"}, false,
		),
	)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	if len(plan.Targets()) != 1 {
		t.Fatalf("combined transition targets = %#v, want the shared assignment-policy serialization target", plan.Targets())
	}
	assertAssignmentPolicyManagedTarget(t, plan, PermissionSetAssignmentPolicySerializationTarget())
	decoded, err := decodeAssignmentPolicySourcePlan(plan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.LossTargets) != 1 || decoded.LossTargets[0].PermissionSet != "Sales_User" ||
		len(decoded.GainTargets) != 1 || decoded.GainTargets[0].PermissionSet != "Standard_User" {
		t.Fatalf("combined source plan = %#v", decoded)
	}
}

func TestAssignmentPolicySourceBatchCanonicalizesSourcesAndTargetUnion(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{{
		"ProfileName": "Sales", "PermissionSetId": "ps-sales",
	}}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	mutations := []AssignmentPolicyEligibilityMutation{
		assignmentPolicyTestEligibilityMutation("user/user-z", []string{"Sales"}, nil, false),
		assignmentPolicyTestEligibilityMutation("user/user-a", nil, []string{"Sales"}, false),
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionsProvider(mutations)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := decodeAssignmentPolicySourcePlan(plan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.Mutations) != 2 || decoded.Mutations[0].SourceIdentity != "user/user-a" ||
		decoded.Mutations[1].SourceIdentity != "user/user-z" {
		t.Fatalf("canonical source mutations = %#v", decoded.Mutations)
	}
	if len(decoded.LossTargets) != 1 || len(decoded.GainTargets) != 1 || len(plan.Targets()) != 1 {
		t.Fatalf("batch target union = losses %#v gains %#v targets %#v", decoded.LossTargets, decoded.GainTargets, plan.Targets())
	}
	if identity := string(plan.ProjectionIdentity()); !strings.Contains(identity, `["user/user-a","user/user-z"]`) {
		t.Fatalf("batch projection identity = %q", identity)
	}
}

func TestAssignmentPolicySourceBatchRejectsConflictingDuplicateSource(t *testing.T) {
	source := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionsProvider([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "record/one", Before: source},
		{
			SourceIdentity: "record/one",
			Before:         source,
			After:          AssignmentPolicyEligibilityState{Selectors: []string{"Standard"}},
		},
	})
	if _, err := provider.Plan(context.Background(), assignmentPolicyLifecycleStore()); err == nil ||
		!strings.Contains(err.Error(), "conflicting transitions") {
		t.Fatalf("conflicting batch source error = %v", err)
	}
}

func TestAssignmentPolicySourceBatchComposesChainedSourceMutations(t *testing.T) {
	absent := AssignmentPolicyEligibilityState{}
	sales := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	standard := AssignmentPolicyEligibilityState{Selectors: []string{"Standard"}}
	canonical, err := canonicalAssignmentPolicySourceMutations([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "user/user-1", Before: absent, After: sales},
		{SourceIdentity: "user/user-1", Before: sales, After: standard},
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(canonical) != 1 || !assignmentPolicyEligibilityStatesEqual(canonical[0].Before, absent) ||
		!assignmentPolicyEligibilityStatesEqual(canonical[0].After, standard) {
		t.Fatalf("composed source mutations = %#v", canonical)
	}
}

func TestAssignmentPolicySourceBatchComposesReverseOrderedChainedSourceMutations(t *testing.T) {
	absent := AssignmentPolicyEligibilityState{}
	sales := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	standard := AssignmentPolicyEligibilityState{Selectors: []string{"Standard"}}
	canonical, err := canonicalAssignmentPolicySourceMutations([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "user/user-1", Before: sales, After: standard},
		{SourceIdentity: "user/user-1", Before: absent, After: sales},
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(canonical) != 1 || !assignmentPolicyEligibilityStatesEqual(canonical[0].Before, absent) ||
		!assignmentPolicyEligibilityStatesEqual(canonical[0].After, standard) {
		t.Fatalf("reverse-ordered source mutations = %#v", canonical)
	}
}

func TestAssignmentPolicySourceBatchKeepsOpaqueIdentitiesByteExact(t *testing.T) {
	absent := AssignmentPolicyEligibilityState{}
	sales := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	canonical, err := canonicalAssignmentPolicySourceMutations([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "A", Before: absent, After: sales},
		{SourceIdentity: "a", Before: sales, After: absent},
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(canonical) != 2 || canonical[0].SourceIdentity != "A" || canonical[1].SourceIdentity != "a" {
		t.Fatalf("byte-exact source identities = %#v", canonical)
	}
	joined := assignmentPolicySourceMutationIdentity([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "A"}, {SourceIdentity: "B"},
	})
	embeddedDelimiter := assignmentPolicySourceMutationIdentity([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "A|B"},
	})
	if joined == embeddedDelimiter {
		t.Fatalf("opaque source identity encoding collided: %q", joined)
	}
}

func TestAssignmentPolicySourceBatchRejectsInvalidUTF8Identity(t *testing.T) {
	invalid := string([]byte{0xff})
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation(invalid, nil, []string{"Sales"}, false),
	)
	if _, err := provider.Plan(context.Background(), assignmentPolicyLifecycleStore()); err == nil ||
		!strings.Contains(err.Error(), "valid UTF-8") {
		t.Fatalf("invalid UTF-8 source identity error = %v", err)
	}
}

func TestAssignmentPolicySourceBatchRejectsDisconnectedSelfTransitions(t *testing.T) {
	sales := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	standard := AssignmentPolicyEligibilityState{Selectors: []string{"Standard"}}
	_, err := canonicalAssignmentPolicySourceMutations([]AssignmentPolicyEligibilityMutation{
		{SourceIdentity: "record/one", Before: sales, After: sales, ReconcileAfterSelectors: true},
		{SourceIdentity: "record/one", Before: standard, After: standard},
	})
	if err == nil || !strings.Contains(err.Error(), "disconnected self transitions") {
		t.Fatalf("disconnected self-transition error = %v", err)
	}
}

func TestAssignmentPolicySourceBatchRejectsNonTerminalReconcileTransition(t *testing.T) {
	absent := AssignmentPolicyEligibilityState{}
	sales := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	standard := AssignmentPolicyEligibilityState{Selectors: []string{"Standard"}}
	_, err := canonicalAssignmentPolicySourceMutations([]AssignmentPolicyEligibilityMutation{
		{
			SourceIdentity: "record/one", Before: absent, After: sales,
			ReconcileAfterSelectors: true,
		},
		{SourceIdentity: "record/one", Before: sales, After: standard},
	})
	if err == nil || !strings.Contains(err.Error(), "non-terminal reconcile transition") {
		t.Fatalf("non-terminal reconcile error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionEstimateCountsTombstonesAndStateWrites(t *testing.T) {
	lossTargets := make([]assignmentPolicySourceTarget, 50)
	for index := range lossTargets {
		lossTargets[index] = assignmentPolicySourceTarget{
			PermissionSetID: fmt.Sprintf("ps-%03d", index), AssignmentRows: assignmentPolicyMaximumRows,
		}
	}
	usage := estimateAssignmentPolicySourceTransition(lossTargets, nil, 0)
	wantStatements := len(lossTargets) * (2*assignmentPolicyMaximumRows + 1)
	if usage.MutationStatements != wantStatements || usage.DeltaRows != wantStatements {
		t.Fatalf("source transition estimate = %#v, want %d statements/delta rows", usage, wantStatements)
	}
	if usage.MutationBytes != wantStatements*assignmentPolicyMutationBytesPerRow {
		t.Fatalf("source transition mutation bytes = %d, want %d", usage.MutationBytes, wantStatements*assignmentPolicyMutationBytesPerRow)
	}
	if err := validateAssignmentPolicySourceTransitionEstimate(usage); err == nil ||
		!strings.Contains(err.Error(), "registration budget") {
		t.Fatalf("oversized source transition estimate error = %v", err)
	}
	if err := validateAssignmentPolicySourceTransitionEstimate(
		estimateAssignmentPolicySourceTransition(lossTargets[:8], nil, 0),
	); err != nil {
		t.Fatalf("in-budget source transition estimate: %v", err)
	}
}

func TestAssignmentPolicySourceTransitionEstimateUsesExhaustedAssignmentPopulation(t *testing.T) {
	lossTargets := make([]assignmentPolicySourceTarget, 49)
	for index := range lossTargets {
		lossTargets[index] = assignmentPolicySourceTarget{
			PermissionSetID: fmt.Sprintf("ps-%03d", index), AssignmentRows: 510,
		}
	}
	usage := estimateAssignmentPolicySourceTransition(lossTargets, nil, 0)
	if usage.MutationStatements != 50029 {
		t.Fatalf("paginated source transition estimate = %#v, want 50029 statements", usage)
	}
	if err := validateAssignmentPolicySourceTransitionEstimate(usage); err == nil ||
		!strings.Contains(err.Error(), "registration budget") {
		t.Fatalf("paginated over-budget source transition error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionRejectsTotalHostCurrentItemWorkBeforeMutation(t *testing.T) {
	store := newPermissionSetDefinitionLifecycleMemoryStore()
	selectors := make([]string, 0, 100)
	for targetIndex := 0; targetIndex < 100; targetIndex++ {
		permissionSetID := fmt.Sprintf("ps-%03d", targetIndex)
		permissionSet := fmt.Sprintf("Set_%03d", targetIndex)
		selector := fmt.Sprintf("Selector %03d", targetIndex)
		selectors = append(selectors, selector)
		store.rows["PermissionSet"] = append(store.rows["PermissionSet"], cp.ComponentRow{
			"Id": permissionSetID, "Name": permissionSet, "OwnerPack": "definitions",
			"IsActive": true, "IsDeleted": false, "IsCustom": true,
			"ProfileId": nil, "IsOwnedByProfile": false,
		})
		store.rows[schemacatalog.AssignmentPolicyClaimTable] = append(
			store.rows[schemacatalog.AssignmentPolicyClaimTable],
			cp.ComponentRow{
				"Id":              fmt.Sprintf("claim-%03d", targetIndex),
				"PermissionSetId": permissionSetID,
				"OwnerPack":       "app",
				"PolicyKey":       fmt.Sprintf("policy-%03d", targetIndex),
			},
		)
		store.rows[schemacatalog.AssignmentPolicySelectorTable] = append(
			store.rows[schemacatalog.AssignmentPolicySelectorTable],
			cp.ComponentRow{"ProfileName": selector, "PermissionSetId": permissionSetID},
		)
		for assignmentIndex := 0; assignmentIndex < 165; assignmentIndex++ {
			store.rows["PermissionSetAssignment"] = append(
				store.rows["PermissionSetAssignment"],
				cp.ComponentRow{
					"Id":              fmt.Sprintf("assignment-%03d-%03d", targetIndex, assignmentIndex),
					"PermissionSetId": permissionSetID,
					"AssigneeId":      fmt.Sprintf("user-%03d-%03d", targetIndex, assignmentIndex),
					"IsDeleted":       false, "LastModifiedDate": "existing",
				},
			)
		}
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", nil, selectors, false),
	)
	if _, err := provider.Plan(context.Background(), store); err == nil ||
		!strings.Contains(err.Error(), "current items") {
		t.Fatalf("total host current-item work error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionPlansExhaustedAssignmentPopulation(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, plan, store,
	); err != nil {
		t.Fatal(err)
	}
	for len(store.rows["PermissionSetAssignment"]) < 510 {
		index := len(store.rows["PermissionSetAssignment"])
		store.rows["PermissionSetAssignment"] = append(
			store.rows["PermissionSetAssignment"],
			cp.ComponentRow{
				"Id": fmt.Sprintf("assignment-%03d", index), "PermissionSetId": "ps-sales",
				"AssigneeId": fmt.Sprintf("user-%03d", index), "IsDeleted": false,
				"LastModifiedDate": "existing",
			},
		)
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", []string{"Sales"}, nil, false),
	)
	transitionPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := decodeAssignmentPolicySourcePlan(transitionPlan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.LossTargets) != 1 || decoded.LossTargets[0].AssignmentRows != 510 {
		t.Fatalf("planned assignment population = %#v", decoded.LossTargets)
	}
	if got := transitionPlan.Estimate().MutationStatements; got != 1023 {
		t.Fatalf("planned paginated mutation statements = %d, want 1023", got)
	}
}

func TestAssignmentPolicySourceTransitionRejectsAssignmentPopulationGrowthBeforeWrites(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, plan, store,
	); err != nil {
		t.Fatal(err)
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", []string{"Sales"}, nil, false),
	)
	transitionPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	store.rows["PermissionSetAssignment"] = append(store.rows["PermissionSetAssignment"], cp.ComponentRow{
		"Id": "assignment-drift", "PermissionSetId": "ps-sales", "AssigneeId": "user-drift",
		"IsDeleted": false, "LastModifiedDate": "existing",
	})
	store.rows["User"][0]["IsActive"] = false
	if _, err := provider.Mutate(context.Background(), transitionPlan, store); err == nil ||
		!strings.Contains(err.Error(), "assignment population grew") {
		t.Fatalf("assignment-population drift error = %v", err)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row["IsDeleted"] != false {
		t.Fatalf("assignment-population drift mutated planned row: %#v", row)
	}
}

func TestAssignmentPolicySourceTransitionBoundsAggregateSelectorQueries(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	before := make([]string, 0, 251)
	after := make([]string, 0, 250)
	for index := 0; index <= assignmentPolicyMaximumRows; index++ {
		selector := fmt.Sprintf("Selector %03d", index)
		store.rows[schemacatalog.AssignmentPolicySelectorTable] = append(
			store.rows[schemacatalog.AssignmentPolicySelectorTable],
			cp.ComponentRow{"ProfileName": selector, "PermissionSetId": "ps-sales"},
		)
		if index < 251 {
			before = append(before, selector)
		} else {
			after = append(after, selector)
		}
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", before, after, false),
	)
	if _, err := provider.Plan(context.Background(), store); err == nil ||
		!strings.Contains(err.Error(), "selector keys") {
		t.Fatalf("aggregate selector-query error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionBoundsAggregateSelectorTargetEdges(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{{
		"ProfileName": "Sales", "PermissionSetId": "ps-sales",
	}}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	planner := newAssignmentPolicySourcePlanner(store)
	planner.selectorEdges = assignmentPolicyMaximumRows
	if _, err := planner.targetsForSelector(context.Background(), "Sales"); err == nil ||
		!strings.Contains(err.Error(), "selector-target edges") {
		t.Fatalf("aggregate selector-target edge error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionRejectsOversizedSelectorMappingBeforeExhaustion(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = nil
	for index := 0; index < 2*assignmentPolicyMaximumRows; index++ {
		store.rows[schemacatalog.AssignmentPolicySelectorTable] = append(
			store.rows[schemacatalog.AssignmentPolicySelectorTable],
			cp.ComponentRow{
				"ProfileName":     "Sales",
				"PermissionSetId": fmt.Sprintf("ps-%04d", index),
			},
		)
	}
	bounded := &assignmentPolicySourceSelectorReadBoundStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
		maximumSelectorRows:                         assignmentPolicyMaximumRows + 1,
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", nil, []string{"Sales"}, false),
	)
	if _, err := provider.Plan(context.Background(), bounded); err == nil ||
		!strings.Contains(err.Error(), "selector-target edges") {
		t.Fatalf("oversized selector mapping error = %v", err)
	}
	if bounded.selectorRows != assignmentPolicyMaximumRows+1 {
		t.Fatalf(
			"oversized selector mapping read %d rows, want bound sentinel %d",
			bounded.selectorRows,
			assignmentPolicyMaximumRows+1,
		)
	}
}

type assignmentPolicySourceSelectorReadBoundStore struct {
	*permissionSetDefinitionLifecycleMemoryStore
	maximumSelectorRows int
	selectorRows        int
}

func (store *assignmentPolicySourceSelectorReadBoundStore) ReadRowsKeysetPage(
	ctx context.Context,
	page cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	result, err := store.permissionSetDefinitionLifecycleMemoryStore.ReadRowsKeysetPage(ctx, page)
	if err != nil || page.Query.Table != schemacatalog.AssignmentPolicySelectorTable {
		return result, err
	}
	store.selectorRows += len(result.Rows)
	if store.selectorRows > store.maximumSelectorRows {
		return cp.ComponentManagedKeysetRowPage{}, fmt.Errorf(
			"selector mapping over-read %d rows", store.selectorRows,
		)
	}
	return result, nil
}

func TestAssignmentPolicySourceTransitionRejectsSelectorMappingGrowthBeforeLockExhaustion(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	itemPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, itemPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", []string{"Sales"}, nil, false),
	)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	for index := 1; index < 2*assignmentPolicyMaximumRows; index++ {
		store.rows[schemacatalog.AssignmentPolicySelectorTable] = append(
			store.rows[schemacatalog.AssignmentPolicySelectorTable],
			cp.ComponentRow{
				"ProfileName":     "Sales",
				"PermissionSetId": fmt.Sprintf("ps-%04d", index),
			},
		)
	}
	bounded := &assignmentPolicySourceSelectorLockBoundStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
		maximumSelectorRows:                         2,
	}
	if _, err := provider.Mutate(context.Background(), plan, bounded); err == nil ||
		!strings.Contains(err.Error(), "selector mapping changed") {
		t.Fatalf("selector mapping growth error = %v", err)
	}
	if bounded.selectorRows != 2 {
		t.Fatalf("selector mapping locked %d rows, want planned-plus-one sentinel 2", bounded.selectorRows)
	}
}

type assignmentPolicySourceSelectorLockBoundStore struct {
	*permissionSetDefinitionLifecycleMemoryStore
	maximumSelectorRows int
	selectorRows        int
}

func (store *assignmentPolicySourceSelectorLockBoundStore) LockRowsKeysetPage(
	ctx context.Context,
	page cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	result, err := store.permissionSetDefinitionLifecycleMemoryStore.LockRowsKeysetPage(ctx, page)
	if err != nil || page.Query.Table != schemacatalog.AssignmentPolicySelectorTable {
		return result, err
	}
	store.selectorRows += len(result.Rows)
	if store.selectorRows > store.maximumSelectorRows {
		return cp.ComponentManagedKeysetRowPage{}, fmt.Errorf(
			"selector mapping over-locked %d rows", store.selectorRows,
		)
	}
	return result, nil
}

func TestAssignmentPolicySourceTransitionRejectsOversizedEligibilityPopulationBeforeReadExhaustion(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	itemPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, itemPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	store.rows["PermissionSetAssignment"] = nil
	maximumCurrentItems := assignmentPolicyManagedBudget().MaxCurrentItems
	bounded := &assignmentPolicySourceEligibilityReadBoundStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
		userRows:        maximumCurrentItems + 1,
		maximumUserRows: maximumCurrentItems + 1,
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", []string{"Sales"}, nil, false),
	)
	if _, err := provider.Plan(context.Background(), bounded); err == nil ||
		!strings.Contains(err.Error(), "eligibility population exceeds maximum") {
		t.Fatalf("oversized eligibility population error = %v", err)
	}
	if bounded.readUserRows > maximumCurrentItems+1 {
		t.Fatalf(
			"oversized eligibility population read %d rows, budget sentinel is %d",
			bounded.readUserRows,
			maximumCurrentItems+1,
		)
	}
}

type assignmentPolicySourceEligibilityReadBoundStore struct {
	*permissionSetDefinitionLifecycleMemoryStore
	userRows        int
	maximumUserRows int
	readUserRows    int
}

func (store *assignmentPolicySourceEligibilityReadBoundStore) ReadRowsKeysetPage(
	ctx context.Context,
	page cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	if page.Query.Table != "User" {
		return store.permissionSetDefinitionLifecycleMemoryStore.ReadRowsKeysetPage(ctx, page)
	}
	result, err := assignmentPolicySourceGeneratedUserPage(page, store.userRows)
	if err != nil {
		return cp.ComponentManagedKeysetRowPage{}, err
	}
	store.readUserRows += len(result.Rows)
	if store.readUserRows > store.maximumUserRows {
		return cp.ComponentManagedKeysetRowPage{}, fmt.Errorf(
			"eligibility population over-read %d User rows", store.readUserRows,
		)
	}
	return result, nil
}

func TestAssignmentPolicySourceTransitionRejectsEligibilityGrowthBeforeReadExhaustion(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	itemPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, itemPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", []string{"Sales"}, nil, false),
	)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	bounded := &assignmentPolicySourceEligibilityMutationReadBoundStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
		userRows:        2 * assignmentPolicyMaximumRows,
		maximumUserRows: 3,
	}
	if _, err := provider.Mutate(context.Background(), plan, bounded); err == nil ||
		!strings.Contains(err.Error(), "eligibility population grew") {
		t.Fatalf("eligibility population growth error = %v", err)
	}
	if bounded.readUserRows != 3 {
		t.Fatalf(
			"eligibility population read %d User rows, want reserved-growth-plus-one sentinel 3",
			bounded.readUserRows,
		)
	}
}

type assignmentPolicySourceEligibilityMutationReadBoundStore struct {
	*permissionSetDefinitionLifecycleMemoryStore
	userRows        int
	maximumUserRows int
	readUserRows    int
}

func (store *assignmentPolicySourceEligibilityMutationReadBoundStore) ReadRowsKeysetPage(
	ctx context.Context,
	page cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	if page.Query.Table != "User" {
		return store.permissionSetDefinitionLifecycleMemoryStore.ReadRowsKeysetPage(ctx, page)
	}
	result, err := assignmentPolicySourceGeneratedUserPage(page, store.userRows)
	if err != nil {
		return cp.ComponentManagedKeysetRowPage{}, err
	}
	store.readUserRows += len(result.Rows)
	if store.readUserRows > store.maximumUserRows {
		return cp.ComponentManagedKeysetRowPage{}, fmt.Errorf(
			"eligibility population over-read %d User rows", store.readUserRows,
		)
	}
	return result, nil
}

func TestAssignmentPolicySourceTransitionRejectsAssignmentGrowthBeforeLockExhaustion(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	itemPlan, err := lifecycle.Lifecycle().Plan(
		context.Background(), cp.ComponentManagedScopeItem, item, store,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, itemPlan, store,
	); err != nil {
		t.Fatal(err)
	}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", []string{"Sales"}, nil, false),
	)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	bounded := &assignmentPolicySourceAssignmentLockBoundStore{
		permissionSetDefinitionLifecycleMemoryStore: store,
		assignmentRows:        2 * assignmentPolicyMaximumRows,
		maximumAssignmentRows: 2,
	}
	if _, err := provider.Mutate(context.Background(), plan, bounded); err == nil ||
		!strings.Contains(err.Error(), "assignment population grew") {
		t.Fatalf("assignment population growth error = %v", err)
	}
	if bounded.lockedAssignmentRows != 2 {
		t.Fatalf(
			"assignment population locked %d rows, want planned-plus-one sentinel 2",
			bounded.lockedAssignmentRows,
		)
	}
}

type assignmentPolicySourceAssignmentLockBoundStore struct {
	*permissionSetDefinitionLifecycleMemoryStore
	assignmentRows        int
	maximumAssignmentRows int
	lockedAssignmentRows  int
}

func (store *assignmentPolicySourceAssignmentLockBoundStore) LockRowsKeysetPage(
	ctx context.Context,
	page cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	if page.Query.Table != "PermissionSetAssignment" {
		return store.permissionSetDefinitionLifecycleMemoryStore.LockRowsKeysetPage(ctx, page)
	}
	result, err := assignmentPolicySourceGeneratedAssignmentPage(page, store.assignmentRows)
	if err != nil {
		return cp.ComponentManagedKeysetRowPage{}, err
	}
	store.lockedAssignmentRows += len(result.Rows)
	if store.lockedAssignmentRows > store.maximumAssignmentRows {
		return cp.ComponentManagedKeysetRowPage{}, fmt.Errorf(
			"assignment population over-locked %d rows", store.lockedAssignmentRows,
		)
	}
	return result, nil
}

func assignmentPolicySourceGeneratedAssignmentPage(
	page cp.ComponentManagedKeysetRowQuery,
	total int,
) (cp.ComponentManagedKeysetRowPage, error) {
	start := 0
	if after, ok := page.ContinueAfter["AssigneeId"]; ok {
		var previous int
		if _, err := fmt.Sscanf(fmt.Sprint(after), "user-%d", &previous); err != nil {
			return cp.ComponentManagedKeysetRowPage{}, err
		}
		start = previous + 1
	}
	count := min(page.Query.MaximumRows, total-start)
	rows := make([]cp.ComponentRow, 0, count)
	for index := start; index < start+count; index++ {
		assigneeID := fmt.Sprintf("user-%06d", index)
		row := cp.ComponentRow{}
		for _, column := range page.Query.Columns {
			switch column {
			case "Id":
				row[column] = fmt.Sprintf("assignment-%06d", index)
			case "PermissionSetId":
				row[column] = "ps-sales"
			case "AssigneeId":
				row[column] = assigneeID
			case "IsDeleted":
				row[column] = false
			case "LastModifiedDate":
				row[column] = "existing"
			}
		}
		rows = append(rows, row)
	}
	return cp.ComponentManagedKeysetRowPage{
		Rows: rows, Exhausted: start+count >= total,
	}, nil
}

func assignmentPolicySourceGeneratedUserPage(
	page cp.ComponentManagedKeysetRowQuery,
	total int,
) (cp.ComponentManagedKeysetRowPage, error) {
	start := 0
	if after, ok := page.ContinueAfter["Id"]; ok {
		var previous int
		if _, err := fmt.Sscanf(fmt.Sprint(after), "user-%d", &previous); err != nil {
			return cp.ComponentManagedKeysetRowPage{}, err
		}
		start = previous + 1
	}
	count := min(page.Query.MaximumRows, total-start)
	rows := make([]cp.ComponentRow, 0, count)
	for index := start; index < start+count; index++ {
		row := cp.ComponentRow{}
		for _, column := range page.Query.Columns {
			switch column {
			case "Id":
				row[column] = fmt.Sprintf("user-%06d", index)
			case "ProfileId":
				row[column] = "profile-sales"
			case "IsActive":
				row[column] = true
			case "IsDeleted":
				row[column] = false
			}
		}
		rows = append(rows, row)
	}
	return cp.ComponentManagedKeysetRowPage{
		Rows: rows, Exhausted: start+count >= total,
	}, nil
}

func TestAssignmentPolicySourceBatchBoundsDistinctSourceIdentities(t *testing.T) {
	mutations := make([]AssignmentPolicyEligibilityMutation, 0, assignmentPolicyMaximumRows+1)
	for index := 0; index <= assignmentPolicyMaximumRows; index++ {
		id := fmt.Sprintf("user-%03d", index)
		mutations = append(mutations,
			assignmentPolicyTestEligibilityMutation("user/"+id, nil, []string{"Sales"}, false),
		)
	}
	if _, err := canonicalAssignmentPolicySourceMutations(mutations); err == nil ||
		!strings.Contains(err.Error(), "maximum") {
		t.Fatalf("oversized source batch error = %v", err)
	}
}

func TestAssignmentPolicySourceBatchCachesSharedProfileTargetDiscovery(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{{
		"ProfileName": "Sales", "PermissionSetId": "ps-sales",
	}}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionsProvider([]AssignmentPolicyEligibilityMutation{
		assignmentPolicyTestEligibilityMutation("user/user-a", nil, []string{"Sales"}, false),
		assignmentPolicyTestEligibilityMutation("user/user-b", nil, []string{"Sales"}, false),
	})
	if _, err := provider.Plan(context.Background(), store); err != nil {
		t.Fatal(err)
	}
	selectorReads := 0
	for _, query := range store.reads {
		switch query.Table {
		case schemacatalog.AssignmentPolicySelectorTable:
			if query.Equal["ProfileName"] == "Sales" {
				selectorReads++
			}
		}
	}
	if selectorReads != 1 {
		t.Fatalf("shared selector discovery reads = %d; reads=%#v", selectorReads, store.reads)
	}
}

func TestAssignmentPolicySourceBatchCachesTargetBindingAcrossSelectors(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{
		{"ProfileName": "Sales", "PermissionSetId": "ps-sales"},
		{"ProfileName": "Revenue", "PermissionSetId": "ps-sales"},
	}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation(
			"record/one", nil, []string{"Revenue", "Sales"}, false,
		),
	)
	if _, err := provider.Plan(context.Background(), store); err != nil {
		t.Fatal(err)
	}
	rootReads := 0
	claimReads := 0
	for _, query := range store.reads {
		if query.Table == "PermissionSet" && query.Equal["Id"] == "ps-sales" {
			rootReads++
		}
		if query.Table == schemacatalog.AssignmentPolicyClaimTable &&
			query.Equal["PermissionSetId"] == "ps-sales" {
			claimReads++
		}
	}
	if rootReads != 1 || claimReads != 1 {
		t.Fatalf("shared target binding reads: root=%d claim=%d; reads=%#v", rootReads, claimReads, store.reads)
	}
}

func TestAssignmentPolicyEligibilityProviderFreezesSelectorInput(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	store.rows[schemacatalog.AssignmentPolicySelectorTable] = []cp.ComponentRow{{
		"ProfileName": "Sales", "PermissionSetId": "ps-sales",
	}}
	store.rows[schemacatalog.AssignmentPolicyClaimTable] = []cp.ComponentRow{{
		"Id": "claim-sales", "PermissionSetId": "ps-sales", "OwnerPack": "app", "PolicyKey": "sales_users",
	}}
	selectors := []string{"Sales"}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/one", nil, selectors, false),
	)
	selectors[0] = "Changed"
	if _, err := provider.Plan(context.Background(), store); err != nil {
		t.Fatalf("provider did not freeze selector input: %v", err)
	}
}

func TestAssignmentPolicyProfileAmbiguityRevokesAndSettlesGeneration(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(context.Background(), cp.ComponentManagedScopeItem, item, plan, store); err != nil {
		t.Fatal(err)
	}
	store.rows["Profile"] = append(store.rows["Profile"], cp.ComponentRow{
		"Id": "profile-renamed", "Name": "Sales", "IsActive": true, "IsDeleted": false,
	})
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation(
			"selector-population/profile-renamed", []string{"Standard"}, []string{"Sales"}, true,
		),
	)
	transitionPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := decodeAssignmentPolicySourcePlan(transitionPlan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.LossTargets) != 1 || len(decoded.GainTargets) != 1 {
		t.Fatalf("Profile ambiguity plan = %#v", decoded)
	}
	result, err := provider.Mutate(context.Background(), transitionPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if result.Disposition != cp.ComponentManagedMutationSafeProjectionStaged {
		t.Fatalf("Profile ambiguity disposition = %q", result.Disposition)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row["IsDeleted"] != true {
		t.Fatalf("Profile ambiguity retained active assignment: %#v", row)
	}
	state := store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]
	if state["ResolutionState"] != assignmentPolicyResolutionInvalidSelector ||
		state["WorkGeneration"] != state["AppliedGeneration"] {
		t.Fatalf("Profile ambiguity state = %#v", state)
	}
}

func TestAssignmentPolicySelectorPopulationReconcileSynchronouslyRestoresMissingGrant(t *testing.T) {
	_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
	if err != nil {
		t.Fatal(err)
	}
	store := assignmentPolicyLifecycleStore()
	item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
	plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := lifecycle.Lifecycle().Mutate(
		context.Background(), cp.ComponentManagedScopeItem, item, plan, store,
	); err != nil {
		t.Fatal(err)
	}
	store.rows["PermissionSetAssignment"] = nil
	state := store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]
	beforeWork := state["WorkGeneration"]

	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation(
			"selector-population/sales", []string{"Sales"}, []string{"Sales"}, true,
		),
	)
	transitionPlan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := decodeAssignmentPolicySourcePlan(transitionPlan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.LossTargets) != 1 || len(decoded.GainTargets) != 1 {
		t.Fatalf("selector-population plan = %#v, want the target in both loss and gain sets", decoded)
	}
	result, err := provider.Mutate(context.Background(), transitionPlan, store)
	if err != nil {
		t.Fatal(err)
	}
	if result.Disposition != cp.ComponentManagedMutationSafeProjectionStaged {
		t.Fatalf("selector-population disposition = %q", result.Disposition)
	}
	state = store.rows[schemacatalog.AssignmentPolicyTargetStateTable][0]
	if state["WorkGeneration"] == beforeWork || state["WorkGeneration"] != state["AppliedGeneration"] {
		t.Fatalf("selector-population gain did not settle synchronously: %#v", state)
	}
	if row := assignmentPolicyTestAssignment(store, "user-sales"); row == nil || row["IsDeleted"] != false {
		t.Fatalf("selector-population reconciliation did not restore the missing grant: %#v", row)
	}
}

func TestAssignmentPolicySourceTransitionRejectsSelectorMappingDriftBeforeMutation(t *testing.T) {
	for _, test := range []struct {
		name  string
		drift func(*permissionSetDefinitionLifecycleMemoryStore)
	}{
		{
			name: "added target",
			drift: func(store *permissionSetDefinitionLifecycleMemoryStore) {
				store.rows[schemacatalog.AssignmentPolicySelectorTable] = append(
					store.rows[schemacatalog.AssignmentPolicySelectorTable],
					cp.ComponentRow{"ProfileName": "Sales", "PermissionSetId": "ps-new"},
				)
			},
		},
		{
			name: "remapped target",
			drift: func(store *permissionSetDefinitionLifecycleMemoryStore) {
				store.rows[schemacatalog.AssignmentPolicySelectorTable][0]["PermissionSetId"] = "ps-new"
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, lifecycle, err := newAssignmentPolicyTestManagedLifecycleRegistration()
			if err != nil {
				t.Fatal(err)
			}
			store := assignmentPolicyLifecycleStore()
			item := cp.NewComponentManagedItemLifecycleRequest(assignmentPolicyManagedRequest(t))
			plan, err := lifecycle.Lifecycle().Plan(context.Background(), cp.ComponentManagedScopeItem, item, store)
			if err != nil {
				t.Fatal(err)
			}
			if _, err := lifecycle.Lifecycle().Mutate(
				context.Background(), cp.ComponentManagedScopeItem, item, plan, store,
			); err != nil {
				t.Fatal(err)
			}
			provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
				assignmentPolicyTestEligibilityMutation("record/user-sales", []string{"Sales"}, nil, false),
			)
			transitionPlan, err := provider.Plan(context.Background(), store)
			if err != nil {
				t.Fatal(err)
			}
			store.rows["User"][0]["IsActive"] = false
			test.drift(store)
			store.locks = nil
			if _, err := provider.Mutate(context.Background(), transitionPlan, store); err == nil ||
				!strings.Contains(err.Error(), "selector mapping changed") {
				t.Fatalf("selector-mapping drift error = %v", err)
			}
			if row := assignmentPolicyTestAssignment(store, "user-sales"); row["IsDeleted"] != false {
				t.Fatalf("selector-mapping drift mutated assignment: %#v", row)
			}
			if len(store.locks) == 0 || store.locks[0].Table != schemacatalog.AssignmentPolicySelectorTable {
				t.Fatalf("selector mapping was not locked before target mutation: %#v", store.locks)
			}
		})
	}
}

func TestAssignmentPolicySourceTransitionSerializesUnmappedSelectorThroughPreparedGlobalTarget(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/new", nil, []string{"Future Selector"}, false),
	)
	plan, err := provider.Plan(context.Background(), store)
	if err != nil {
		t.Fatal(err)
	}
	if len(plan.Targets()) != 1 {
		t.Fatalf("unmapped-selector targets = %#v, want only the pre-preparable global serialization target", plan.Targets())
	}
	assertAssignmentPolicyManagedTarget(t, plan, PermissionSetAssignmentPolicySerializationTarget())
	decoded, err := decodeAssignmentPolicySourcePlan(plan)
	if err != nil {
		t.Fatal(err)
	}
	if len(decoded.SelectorMapping) != 1 || decoded.SelectorMapping[0].Selector != "Future Selector" ||
		len(decoded.SelectorMapping[0].PermissionSetIDs) != 0 {
		t.Fatalf("unmapped selector plan = %#v", decoded.SelectorMapping)
	}
	result, err := provider.Mutate(context.Background(), plan, store)
	if err != nil {
		t.Fatal(err)
	}
	if result.Disposition != cp.ComponentManagedMutationNoChange {
		t.Fatalf("unmapped-selector disposition = %q", result.Disposition)
	}
	if len(store.locks) == 0 || store.locks[0].Table != schemacatalog.AssignmentPolicySelectorTable {
		t.Fatalf("unmapped selector mapping was not locked: %#v", store.locks)
	}
	if got := strings.Join(store.locks[0].LogicalKeyColumns, ","); got != "PermissionSetId,ProfileName" {
		t.Fatalf("unmapped selector lock key = %q, want live unique key", got)
	}
	lockOrder := make([]string, 0, len(store.locks[0].OrderBy))
	for _, order := range store.locks[0].OrderBy {
		lockOrder = append(lockOrder, order.Column)
	}
	if got := strings.Join(lockOrder, ","); got != "PermissionSetId,ProfileName" {
		t.Fatalf("unmapped selector lock order = %q, want keyset order", got)
	}
}

func TestAssignmentPolicySourceTransitionReturnsNoTargetsWithoutGlobalLock(t *testing.T) {
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/unrelated", nil, nil, false),
	)
	if _, err := provider.Plan(context.Background(), assignmentPolicyLifecycleStore()); !errors.Is(err, cp.ErrComponentManagedNoTargets) {
		t.Fatalf("no-target discovery error = %v", err)
	}
}

func TestAssignmentPolicySourceTransitionReturnsNoTargetsForEmptySelectorReconcile(t *testing.T) {
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
		assignmentPolicyTestEligibilityMutation("record/unrelated", nil, nil, true),
	)
	if _, err := provider.Plan(context.Background(), assignmentPolicyLifecycleStore()); !errors.Is(err, cp.ErrComponentManagedNoTargets) {
		t.Fatalf("empty-selector reconcile error = %v, want no targets", err)
	}
}

func TestAssignmentPolicySourceTransitionSkipsUnchangedEligibility(t *testing.T) {
	source := AssignmentPolicyEligibilityState{Selectors: []string{"Sales"}}
	provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(AssignmentPolicyEligibilityMutation{
		SourceIdentity: "record/user-sales", Before: source, After: source,
	})
	if _, err := provider.Plan(context.Background(), assignmentPolicyLifecycleStore()); !errors.Is(err, cp.ErrComponentManagedNoTargets) {
		t.Fatalf("unchanged source discovery error = %v", err)
	}
}

func TestAssignmentPolicyEligibilityMutationRejectsNonCanonicalSelector(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	for _, selector := range []string{" Sales", "Sales ", " Sales "} {
		provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(
			assignmentPolicyTestEligibilityMutation(
				"record/one", nil, []string{selector}, false,
			),
		)
		if _, err := provider.Plan(context.Background(), store); err == nil ||
			!strings.Contains(err.Error(), "canonical") {
			t.Fatalf("selector %q error = %v, want canonical-selector rejection", selector, err)
		}
	}
}

func TestAssignmentPolicyEligibilityMutationRejectsMissingIdentityOrEmptySelector(t *testing.T) {
	store := assignmentPolicyLifecycleStore()
	for _, mutation := range []AssignmentPolicyEligibilityMutation{
		{},
		{SourceIdentity: "record/one", After: AssignmentPolicyEligibilityState{Selectors: []string{""}}},
	} {
		provider := newTestPermissionSetAssignmentPolicyEligibilityTransitionProvider(mutation)
		if _, err := provider.Plan(context.Background(), store); err == nil {
			t.Fatalf("source mutation %#v was accepted", mutation)
		}
	}
}

func assignmentPolicyTestEligibilityMutation(
	identity string,
	before []string,
	after []string,
	reconcileAfter bool,
) AssignmentPolicyEligibilityMutation {
	return AssignmentPolicyEligibilityMutation{
		SourceIdentity:          identity,
		Before:                  AssignmentPolicyEligibilityState{Selectors: before},
		After:                   AssignmentPolicyEligibilityState{Selectors: after},
		ReconcileAfterSelectors: reconcileAfter,
	}
}
