package permissions

import (
	"context"
	"errors"
	"reflect"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

func TestPermissionAuthoringSaveCommitsOneAggregateAndTerminalFirstReplay(t *testing.T) {
	fixture := newPermissionAuthoringFixture(t)
	request := permissionAuthoringRequestForTest(t)

	first, err := fixture.service.Save(fixture.ctx, fixture.invocation, request)
	if err != nil {
		t.Fatalf("Save(first): %v", err)
	}
	if first.Outcome != permissionauthoringv1.OutcomeApplied ||
		first.AuthoringToken != permissionAuthoringTokenForTest(t, "permission-set-1", 8) {
		t.Fatalf("Save(first) = %+v", first)
	}
	if fixture.transactions.calls != 3 {
		t.Fatalf("pessimistic transactions = %d, want accept + bind + execute", fixture.transactions.calls)
	}
	if fixture.plan.applyCalls != 1 || fixture.planner.planCalls != 1 || fixture.targets.advanceCalls != 1 ||
		fixture.audit.calls != 1 || fixture.state.calls != 1 || fixture.convergence.enqueueCalls != 1 || fixture.convergence.wakeCalls != 1 {
		t.Fatalf("aggregate effects: apply=%d plan=%d revision=%d audit=%d state=%d enqueue=%d wake=%d",
			fixture.plan.applyCalls, fixture.planner.planCalls, fixture.targets.advanceCalls,
			fixture.audit.calls, fixture.state.calls, fixture.convergence.enqueueCalls, fixture.convergence.wakeCalls)
	}
	if fixture.convergence.event.CommandRowID != "command-row-1" ||
		fixture.convergence.event.CanonicalPermissionSet != "permission-set-1" ||
		fixture.convergence.event.AuthoringRevision != 8 {
		t.Fatalf("durable convergence identity = %+v", fixture.convergence.event)
	}
	if fixture.audit.event.AuthoringRevision != 8 || permissionAuthoringChangeCount(fixture.audit.event.FinalChanges) != 1 {
		t.Fatalf("aggregate audit identity = %+v", fixture.audit.event)
	}
	executeTx := fixture.plan.applyTransaction
	if executeTx == 0 || fixture.audit.transaction != executeTx || fixture.state.transaction != executeTx ||
		fixture.convergence.enqueueTransaction != executeTx || fixture.commands.completeTransaction != executeTx {
		t.Fatalf("aggregate escaped one transaction: apply=%d audit=%d state=%d enqueue=%d complete=%d",
			executeTx, fixture.audit.transaction, fixture.state.transaction,
			fixture.convergence.enqueueTransaction, fixture.commands.completeTransaction)
	}

	// A retry may raise its fresh-admission fence. Exact replay still returns
	// the frozen terminal outcome before mutable target lookup, authorization,
	// receipts, or another transaction.
	higherFence := uint64(999)
	request.MinCatalogRevision = &higherFence
	fixture.targets.resolveErr = errors.New("target lookup must not run during terminal replay")
	fixture.policy.authorizeErr = errors.New("authorization must not run during terminal replay")
	beforeReceipts := fixture.receipts.calls
	beforeTransactions := fixture.transactions.calls
	replayed, err := fixture.service.Save(fixture.ctx, fixture.invocation, request)
	if err != nil {
		t.Fatalf("Save(replay): %v", err)
	}
	if !reflect.DeepEqual(replayed, first) {
		t.Fatalf("Save(replay) = %+v, want frozen %+v", replayed, first)
	}
	if fixture.receipts.calls != beforeReceipts || fixture.transactions.calls != beforeTransactions || fixture.targets.resolveCalls != 1 {
		t.Fatalf("replay repeated mutable work: receipts %d->%d tx %d->%d resolve=%d",
			beforeReceipts, fixture.receipts.calls, beforeTransactions, fixture.transactions.calls, fixture.targets.resolveCalls)
	}
}

func TestPermissionAuthoringLateApplyFailureCannotCommitTerminalSuccess(t *testing.T) {
	fixture := newPermissionAuthoringFixture(t)
	fixture.plan.applyErr = errors.New("late field writer failure")

	_, err := fixture.service.Save(fixture.ctx, fixture.invocation, permissionAuthoringRequestForTest(t))
	if err == nil || err.Error() != "late field writer failure" {
		t.Fatalf("Save() error = %v", err)
	}
	row := fixture.commands.onlyRow(t)
	if row.State != ports.PermissionAuthoringCommandAccepted || len(row.ResultPayload) != 0 {
		t.Fatalf("failed mutation became terminal: %+v", row)
	}
	if fixture.transactions.commits != 2 || fixture.transactions.rollbacks != 1 {
		t.Fatalf("transactions commits=%d rollbacks=%d, want accepted+bound then mutation rollback",
			fixture.transactions.commits, fixture.transactions.rollbacks)
	}
	if fixture.audit.calls != 0 || fixture.targets.advanceCalls != 0 || fixture.state.calls != 0 ||
		fixture.convergence.enqueueCalls != 0 || fixture.convergence.wakeCalls != 0 || fixture.commands.completeCalls != 0 {
		t.Fatalf("rollback leaked success effects: audit=%d revision=%d state=%d enqueue=%d wake=%d complete=%d",
			fixture.audit.calls, fixture.targets.advanceCalls, fixture.state.calls,
			fixture.convergence.enqueueCalls, fixture.convergence.wakeCalls, fixture.commands.completeCalls)
	}
}

func TestPermissionAuthoringAcceptedRetryRequiresCurrentCatalogFenceBeforeMutation(t *testing.T) {
	fixture := newPermissionAuthoringFixture(t)
	request := permissionAuthoringRequestForTest(t)
	fixture.plan.applyErr = errors.New("late field writer failure")

	if _, err := fixture.service.Save(fixture.ctx, fixture.invocation, request); err == nil {
		t.Fatal("initial execution failure returned nil")
	}
	if row := fixture.commands.onlyRow(t); row.State != ports.PermissionAuthoringCommandAccepted {
		t.Fatalf("failed command state = %s, want ACCEPTED", row.State)
	}
	beforeTransactions := fixture.transactions.calls
	beforePlans := fixture.planner.planCalls
	beforeApplies := fixture.plan.applyCalls

	higherFence := uint64(999)
	request.MinCatalogRevision = &higherFence
	_, err := fixture.service.Save(fixture.ctx, fixture.invocation, request)
	var failure *permissionauthoringv1.Failure
	if !errors.As(err, &failure) || failure.Category != permissionauthoringv1.FailureStaleState {
		t.Fatalf("Save(accepted retry above serving fence) error = %v, want STALE_STATE", err)
	}
	if fixture.transactions.calls != beforeTransactions || fixture.planner.planCalls != beforePlans ||
		fixture.plan.applyCalls != beforeApplies || fixture.targets.advanceCalls != 0 || fixture.audit.calls != 0 ||
		fixture.state.calls != 0 || fixture.convergence.enqueueCalls != 0 || fixture.commands.completeCalls != 0 {
		t.Fatalf("stale accepted retry reached execution: tx=%d->%d plan=%d->%d apply=%d->%d revision=%d audit=%d state=%d enqueue=%d complete=%d",
			beforeTransactions, fixture.transactions.calls, beforePlans, fixture.planner.planCalls,
			beforeApplies, fixture.plan.applyCalls, fixture.targets.advanceCalls, fixture.audit.calls,
			fixture.state.calls, fixture.convergence.enqueueCalls, fixture.commands.completeCalls)
	}

	fixture.receipts.value.CatalogRevision = higherFence
	fixture.plan.applyErr = nil
	response, err := fixture.service.Save(fixture.ctx, fixture.invocation, request)
	if err != nil {
		t.Fatalf("Save(accepted retry after serving catches up): %v", err)
	}
	if response.Outcome != permissionauthoringv1.OutcomeApplied || fixture.plan.applyCalls != beforeApplies+1 {
		t.Fatalf("caught-up accepted retry response=%+v applyCalls=%d, want APPLIED and %d",
			response, fixture.plan.applyCalls, beforeApplies+1)
	}
}

func TestPermissionAuthoringSaveRefusesNonterminalCommandFromOldSemanticRevision(t *testing.T) {
	fixture := newPermissionAuthoringFixture(t)
	request := permissionAuthoringRequestForTest(t)
	digest, err := permissionauthoringv1.CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	scope := permissionAuthoringScopeDigest(fixture.invocation.AuthorityScope, fixture.actor.ID)
	row := ports.PermissionAuthoringCommand{
		RowID: "old-row", ScopeDigest: scope, CommandID: request.CommandID,
		RequestDigest: digest, TargetKind: string(request.Target.Kind), TargetID: request.Target.ID,
		ExpectedAuthoringToken:  request.ExpectedAuthoringToken,
		SemanticMappingRevision: permissionauthoringv1.SemanticMappingRevision - 1,
		State:                   ports.PermissionAuthoringCommandAccepted,
	}
	fixture.commands.byKey[commandKey(scope, request.CommandID)] = row
	fixture.commands.keyByRow[row.RowID] = commandKey(scope, request.CommandID)

	_, err = fixture.service.Save(fixture.ctx, fixture.invocation, request)
	var failure *permissionauthoringv1.Failure
	if !errors.As(err, &failure) || failure.Category != permissionauthoringv1.FailureIncompatible {
		t.Fatalf("Save(old semantic command) = %T(%v)", err, err)
	}
	if fixture.targets.resolveCalls != 0 || fixture.planner.planCalls != 0 {
		t.Fatalf("old semantic command executed: resolve=%d plan=%d", fixture.targets.resolveCalls, fixture.planner.planCalls)
	}
}

func TestPermissionAuthoringStaleTokenIsDurableWithoutFamilyWrites(t *testing.T) {
	fixture := newPermissionAuthoringFixture(t)
	fixture.targets.revision = 9

	response, err := fixture.service.Save(fixture.ctx, fixture.invocation, permissionAuthoringRequestForTest(t))
	if err != nil {
		t.Fatalf("Save(): %v", err)
	}
	if response.Outcome != permissionauthoringv1.OutcomeRejected ||
		response.RejectionCode != permissionauthoringv1.RejectionStaleAuthoringState || response.AuthoringToken != "" {
		t.Fatalf("Save() = %+v", response)
	}
	if fixture.planner.planCalls != 0 || fixture.plan.applyCalls != 0 || fixture.audit.calls != 0 ||
		fixture.targets.advanceCalls != 0 || fixture.state.calls != 0 || fixture.convergence.enqueueCalls != 0 {
		t.Fatal("stale command reached mutation or success side effects")
	}
	if row := fixture.commands.onlyRow(t); row.State != ports.PermissionAuthoringCommandCompleted {
		t.Fatalf("stale command state = %s, want durable COMPLETED rejection", row.State)
	}
}

func TestPermissionAuthoringPackOwnerUsesNullBoundary(t *testing.T) {
	for _, test := range []struct {
		name    string
		owner   any
		managed bool
	}{
		{name: "null administrator owned", owner: nil, managed: false},
		{name: "named pack owned", owner: "official:security", managed: true},
		{name: "blank remains pack owned", owner: "", managed: true},
		{name: "whitespace remains pack owned", owner: " ", managed: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			record := models.SObject{"OwnerPack": test.owner}
			if got := permissionSetOwnerPackManaged(record); got != test.managed {
				t.Fatalf("permissionSetOwnerPackManaged() = %t, want %t", got, test.managed)
			}
		})
	}
}

type permissionAuthoringFixture struct {
	ctx          context.Context
	actor        *models.UserSession
	invocation   PermissionAuthoringInvocation
	transactions *fakePermissionAuthoringTransactions
	commands     *fakePermissionAuthoringCommands
	targets      *fakePermissionAuthoringTargets
	policy       *fakePermissionAuthoringPolicy
	planner      *fakePermissionAuthoringPlanner
	plan         *fakePermissionAuthoringPlan
	audit        *fakePermissionAuthoringAudit
	state        *fakePermissionAuthoringState
	convergence  *fakePermissionAuthoringConvergence
	receipts     *fakePermissionAuthoringReceipts
	service      *PermissionAuthoringService
}

func newPermissionAuthoringFixture(t *testing.T) *permissionAuthoringFixture {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	t.Cleanup(cancel)
	fixture := &permissionAuthoringFixture{
		ctx:          ctx,
		actor:        &models.UserSession{ID: "user-1", ProfileID: "profile-admin", ProfileName: "Administrator"},
		transactions: &fakePermissionAuthoringTransactions{},
		commands:     newFakePermissionAuthoringCommands(),
		targets:      &fakePermissionAuthoringTargets{permissionSetID: "permission-set-1", found: true, revision: 7},
		policy:       &fakePermissionAuthoringPolicy{authorized: true},
		plan:         &fakePermissionAuthoringPlan{changed: true},
		audit:        &fakePermissionAuthoringAudit{},
		state:        &fakePermissionAuthoringState{},
		convergence:  &fakePermissionAuthoringConvergence{},
		receipts: &fakePermissionAuthoringReceipts{value: permissionauthoringv1.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: permissionauthoringv1.CapabilityName,
			CatalogRevision: 7, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 11,
		}},
	}
	fixture.invocation = PermissionAuthoringInvocation{
		AuthorityScope:               "installation-1",
		Actor:                        fixture.actor,
		AdmittedPermissionStateFloor: permissionAuthoringPermissionStateVersionForTest(t, 11),
	}
	fixture.planner = &fakePermissionAuthoringPlanner{plan: fixture.plan}
	service, err := NewPermissionAuthoringService(
		fixture.transactions, fixture.commands, fixture.targets, fixture.policy,
		fixture.planner, fixture.audit, fixture.state, fixture.convergence, fixture.receipts,
		PermissionAuthoringServiceOptions{
			AdmissionTTL:     5 * time.Minute,
			OutcomeRetention: time.Hour,
			NewRowID:         func() string { return "command-row-1" },
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	fixture.service = service
	return fixture
}

func permissionAuthoringRequestForTest(t *testing.T) permissionauthoringv1.Request {
	t.Helper()
	fence := uint64(7)
	return permissionauthoringv1.Request{
		CommandID:              "permission-save:1",
		Target:                 permissionauthoringv1.Target{Kind: permissionauthoringv1.TargetProfile, ID: "profile-sales"},
		ExpectedAuthoringToken: permissionAuthoringTokenForTest(t, "permission-set-1", 7),
		MinCatalogRevision:     &fence,
		Changes: permissionauthoringv1.Changes{
			System:      []permissionauthoringv1.CapabilityGrantChange{{CapabilityID: "ManageUsers", Enabled: true}},
			Objects:     []permissionauthoringv1.ObjectGrantChange{{ResourceType: "Account", Read: true, Edit: true}},
			Fields:      []permissionauthoringv1.FieldGrantChange{{ResourceType: "Account", FieldName: "Name", Read: true}},
			RecordTypes: []permissionauthoringv1.RecordTypeGrantChange{{RecordTypeID: "record-type-1", ResourceType: "Account", Visible: true}},
			Custom:      []permissionauthoringv1.CapabilityGrantChange{{CapabilityID: "custom-permission-1", Enabled: true}},
		},
	}
}

func permissionAuthoringTokenForTest(t *testing.T, permissionSetID string, revision uint64) string {
	t.Helper()
	token, err := permissionauthoringv1.NewAuthoringToken(permissionSetID, revision)
	if err != nil {
		t.Fatal(err)
	}
	return token
}

type permissionAuthoringTxKey struct{}

type fakePermissionAuthoringTransactions struct {
	calls, commits, rollbacks int
}

func (f *fakePermissionAuthoringTransactions) WithCurrentReadPermissionAuthoringTransaction(ctx context.Context, fn func(context.Context) error) error {
	f.calls++
	txCtx := context.WithValue(ctx, permissionAuthoringTxKey{}, f.calls)
	txCtx = ports.WithAfterCommitHooks(txCtx)
	if err := fn(txCtx); err != nil {
		f.rollbacks++
		return err
	}
	f.commits++
	ports.RunAfterCommitHooks(txCtx)
	return nil
}

func permissionAuthoringTx(ctx context.Context) int {
	value, _ := ctx.Value(permissionAuthoringTxKey{}).(int)
	return value
}

type fakePermissionAuthoringCommands struct {
	byKey               map[string]ports.PermissionAuthoringCommand
	keyByRow            map[string]string
	completeCalls       int
	completeTransaction int
}

func newFakePermissionAuthoringCommands() *fakePermissionAuthoringCommands {
	return &fakePermissionAuthoringCommands{byKey: map[string]ports.PermissionAuthoringCommand{}, keyByRow: map[string]string{}}
}

func commandKey(scope, command string) string { return scope + "\x00" + command }

func (f *fakePermissionAuthoringCommands) LoadActive(_ context.Context, scope, command string) (ports.PermissionAuthoringCommand, error) {
	row, ok := f.byKey[commandKey(scope, command)]
	if !ok {
		return row, ports.ErrPermissionAuthoringCommandMissing
	}
	return row, nil
}

func (f *fakePermissionAuthoringCommands) Accept(_ context.Context, input ports.PermissionAuthoringCommandAcceptance) (ports.PermissionAuthoringCommand, bool, error) {
	key := commandKey(input.ScopeDigest, input.CommandID)
	if existing, ok := f.byKey[key]; ok {
		if existing.RequestDigest != input.RequestDigest {
			return existing, true, ports.ErrPermissionAuthoringCommandConflict
		}
		return existing, true, nil
	}
	row := ports.PermissionAuthoringCommand{
		RowID: input.RowID, ScopeDigest: input.ScopeDigest, CommandID: input.CommandID,
		RequestDigest: input.RequestDigest, TargetKind: input.TargetKind, TargetID: input.TargetID,
		ExpectedAuthoringToken:  input.ExpectedAuthoringToken,
		SemanticMappingRevision: input.SemanticMappingRevision,
		State:                   ports.PermissionAuthoringCommandAccepted,
		AdmissionExpiresAt: time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC).
			Add(input.AdmissionTTL),
		OutcomeRetentionMicros: input.OutcomeRetention.Microseconds(),
	}
	f.byKey[key] = row
	f.keyByRow[row.RowID] = key
	return row, false, nil
}

func (f *fakePermissionAuthoringCommands) LockCommand(_ context.Context, rowID string) (ports.PermissionAuthoringCommand, error) {
	key, ok := f.keyByRow[rowID]
	if !ok {
		return ports.PermissionAuthoringCommand{}, ports.ErrPermissionAuthoringCommandMissing
	}
	return f.byKey[key], nil
}

func (f *fakePermissionAuthoringCommands) BindCanonicalPermissionSet(_ context.Context, rowID, permissionSetID string) error {
	key := f.keyByRow[rowID]
	row := f.byKey[key]
	if row.CanonicalPermissionSetID != "" && row.CanonicalPermissionSetID != permissionSetID {
		return ports.ErrPermissionAuthoringBindingConflict
	}
	row.CanonicalPermissionSetID = permissionSetID
	f.byKey[key] = row
	return nil
}

func (f *fakePermissionAuthoringCommands) TerminalOutcomeExpiry(_ context.Context, rowID string) (time.Time, error) {
	key, ok := f.keyByRow[rowID]
	if !ok {
		return time.Time{}, ports.ErrPermissionAuthoringCommandMissing
	}
	row := f.byKey[key]
	return time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC).
		Add(time.Duration(row.OutcomeRetentionMicros) * time.Microsecond), nil
}

func (f *fakePermissionAuthoringCommands) CompleteBound(ctx context.Context, rowID string, payload []byte, expiry time.Time) error {
	key := f.keyByRow[rowID]
	row := f.byKey[key]
	if row.CanonicalPermissionSetID == "" {
		return errors.New("bound completion has no target")
	}
	f.complete(ctx, key, row, payload, expiry)
	return nil
}

func (f *fakePermissionAuthoringCommands) CompleteUnboundRejection(ctx context.Context, rowID string, payload []byte, expiry time.Time) error {
	key := f.keyByRow[rowID]
	row := f.byKey[key]
	if row.CanonicalPermissionSetID != "" {
		return errors.New("unbound completion has a target")
	}
	f.complete(ctx, key, row, payload, expiry)
	return nil
}

func (f *fakePermissionAuthoringCommands) complete(ctx context.Context, key string, row ports.PermissionAuthoringCommand, payload []byte, expiry time.Time) {
	f.completeCalls++
	f.completeTransaction = permissionAuthoringTx(ctx)
	row.State = ports.PermissionAuthoringCommandCompleted
	row.ResultPayload = append([]byte(nil), payload...)
	row.OutcomeExpiresAt = expiry
	f.byKey[key] = row
}

func (f *fakePermissionAuthoringCommands) onlyRow(t *testing.T) ports.PermissionAuthoringCommand {
	t.Helper()
	if len(f.byKey) != 1 {
		t.Fatalf("command rows = %d, want 1", len(f.byKey))
	}
	for _, row := range f.byKey {
		return row
	}
	return ports.PermissionAuthoringCommand{}
}

type fakePermissionAuthoringTargets struct {
	permissionSetID string
	found           bool
	revision        uint64
	resolveCalls    int
	advanceCalls    int
	resolveErr      error
}

func (f *fakePermissionAuthoringTargets) ResolvePermissionAuthoringTarget(context.Context, string, string) (string, bool, error) {
	f.resolveCalls++
	return f.permissionSetID, f.found, f.resolveErr
}

func (f *fakePermissionAuthoringTargets) LockPermissionSetAuthoring(_ context.Context, permissionSetID string) (ports.LockedPermissionAuthoringAggregate, bool, error) {
	return ports.LockedPermissionAuthoringAggregate{
		CanonicalPermissionSetID: permissionSetID,
		Role:                     ports.PermissionAuthoringProfileBaseline,
		Revision:                 f.revision,
	}, f.found, nil
}

func (f *fakePermissionAuthoringTargets) LockPermissionAuthoringCustomPermissions(context.Context, []string) error {
	return nil
}

func (f *fakePermissionAuthoringTargets) AdvancePermissionSetAuthoringRevision(_ context.Context, _ string, expected uint64) (uint64, error) {
	f.advanceCalls++
	if expected != f.revision {
		return 0, errors.New("unexpected authoring revision")
	}
	f.revision++
	return f.revision, nil
}

type fakePermissionAuthoringPolicy struct {
	authorized   bool
	authorizeErr error
	code         permissionauthoringv1.RejectionCode
}

func (f *fakePermissionAuthoringPolicy) AuthorizePermissionSetAuthoring(
	_ context.Context,
	_ permissionstatev1.Version,
	actor *models.UserSession,
) (*PermissionAuthoringAuthorization, bool, error) {
	return &PermissionAuthoringAuthorization{
		FormulaActor:      actor,
		SystemPermissions: map[string]bool{string(constants.PermManageProfilesPermSets): f.authorized},
	}, f.authorized, f.authorizeErr
}

func (f *fakePermissionAuthoringPolicy) CheckPermissionSetAuthorable(context.Context, string) (permissionauthoringv1.RejectionCode, error) {
	return f.code, nil
}

type fakePermissionAuthoringPlanner struct {
	plan      *fakePermissionAuthoringPlan
	planCalls int
}

func (f *fakePermissionAuthoringPlanner) PlanPermissionAuthoringMutation(context.Context, ports.LockedPermissionAuthoringAggregate, permissionauthoringv1.Changes, *models.UserSession) (PermissionAuthoringMutationPlan, error) {
	f.planCalls++
	return f.plan, nil
}

type fakePermissionAuthoringPlan struct {
	changed          bool
	applyCalls       int
	applyTransaction int
	applyErr         error
}

func (f *fakePermissionAuthoringPlan) Changed() bool { return f.changed }
func (f *fakePermissionAuthoringPlan) FinalChanges() permissionauthoringv1.Changes {
	if !f.changed {
		return permissionauthoringv1.Changes{}
	}
	return permissionauthoringv1.Changes{System: []permissionauthoringv1.CapabilityGrantChange{{CapabilityID: "ManageUsers", Enabled: true}}}
}
func (f *fakePermissionAuthoringPlan) Apply(ctx context.Context) error {
	f.applyCalls++
	f.applyTransaction = permissionAuthoringTx(ctx)
	return f.applyErr
}

type fakePermissionAuthoringAudit struct {
	calls       int
	transaction int
	event       PermissionAuthoringAuditEvent
}

func (f *fakePermissionAuthoringAudit) WritePermissionAuthoringAudit(ctx context.Context, event PermissionAuthoringAuditEvent) error {
	f.calls++
	f.transaction = permissionAuthoringTx(ctx)
	f.event = event
	return nil
}

type fakePermissionAuthoringState struct {
	calls       int
	transaction int
}

func (f *fakePermissionAuthoringState) RequestAdvanceIfChanged(ctx context.Context, changed bool) error {
	if !changed {
		return errors.New("aggregate requested unchanged Permission State advance")
	}
	f.calls++
	f.transaction = permissionAuthoringTx(ctx)
	return nil
}

type fakePermissionAuthoringConvergence struct {
	enqueueCalls       int
	enqueueTransaction int
	wakeCalls          int
	event              ports.PermissionAuthoringConvergenceEvent
}

func (f *fakePermissionAuthoringConvergence) EnqueuePermissionAuthoringConvergence(ctx context.Context, event ports.PermissionAuthoringConvergenceEvent) error {
	f.enqueueCalls++
	f.enqueueTransaction = permissionAuthoringTx(ctx)
	f.event = event
	return nil
}

func (f *fakePermissionAuthoringConvergence) WakePermissionAuthoringConvergence() { f.wakeCalls++ }

type fakePermissionAuthoringReceipts struct {
	value permissionauthoringv1.Receipts
	calls int
}

func (f *fakePermissionAuthoringReceipts) PermissionAuthoringReceipts(context.Context) (permissionauthoringv1.Receipts, error) {
	f.calls++
	return f.value, nil
}

func permissionAuthoringPermissionStateVersionForTest(t *testing.T, revision uint64) permissionstatev1.Version {
	t.Helper()
	lineage, err := permissionstatev1.LineageFromBytes([]byte("0123456789abcdef"))
	if err != nil {
		t.Fatal(err)
	}
	version, err := permissionstatev1.NewVersion(lineage, revision)
	if err != nil {
		t.Fatal(err)
	}
	return version
}
