package records

import (
	"context"
	"strings"
	"testing"
	"time"

	eventFeature "github.com/nexuscrm/events"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/kernel/pkg/validation"
	metadataFeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

type inlineTxManager struct{}

type inlineTxActiveKey struct{}
type inlineTxPessimisticKey struct{}
type inlineTxIdentityKey struct{}

func (inlineTxManager) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	txCtx := context.WithValue(ctx, inlineTxActiveKey{}, true)
	txCtx = context.WithValue(txCtx, inlineTxPessimisticKey{}, false)
	txCtx = context.WithValue(txCtx, inlineTxIdentityKey{}, &struct{}{})
	txCtx = ports.WithTransactionAbort(txCtx)
	txCtx = ports.WithTransactionCompletionHooks(txCtx)
	err := fn(txCtx)
	if err != nil {
		ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionRolledBack)
		return err
	}
	if abortErr := ports.PrepareTransactionCommit(txCtx); abortErr != nil {
		ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionRolledBack)
		return abortErr
	}
	ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionCommitted)
	return nil
}
func (inlineTxManager) WithPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	txCtx := context.WithValue(ctx, inlineTxActiveKey{}, true)
	txCtx = context.WithValue(txCtx, inlineTxPessimisticKey{}, true)
	txCtx = context.WithValue(txCtx, inlineTxIdentityKey{}, &struct{}{})
	txCtx = ports.WithTransactionAbort(txCtx)
	txCtx = ports.WithTransactionCompletionHooks(txCtx)
	err := fn(txCtx)
	if err != nil {
		ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionRolledBack)
		return err
	}
	if abortErr := ports.PrepareTransactionCommit(txCtx); abortErr != nil {
		ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionRolledBack)
		return abortErr
	}
	ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionCommitted)
	return nil
}
func (inlineTxManager) WithRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return inlineTxManager{}.WithTransaction(ctx, fn)
}
func (inlineTxManager) WithPessimisticRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return inlineTxManager{}.WithPessimisticTransaction(ctx, fn)
}
func (inlineTxManager) IsTransactionActive(ctx context.Context) bool {
	active, _ := ctx.Value(inlineTxActiveKey{}).(bool)
	return active
}
func (inlineTxManager) IsPessimisticTx(ctx context.Context) bool {
	pessimistic, _ := ctx.Value(inlineTxPessimisticKey{}).(bool)
	return pessimistic
}
func (inlineTxManager) ExtractTx(ctx context.Context) interface{} {
	return ctx.Value(inlineTxIdentityKey{})
}
func (inlineTxManager) ReadTransactionTime(context.Context) (time.Time, error) {
	return time.Date(2026, 7, 17, 0, 0, 0, 0, time.UTC), nil
}
func (inlineTxManager) DetachTx(ctx context.Context) context.Context {
	ctx = context.WithValue(ctx, inlineTxActiveKey{}, false)
	ctx = context.WithValue(ctx, inlineTxPessimisticKey{}, false)
	return context.WithValue(ctx, inlineTxIdentityKey{}, nil)
}

type noopRollupProcessor struct{}

func (noopRollupProcessor) ProcessInsertRollups(context.Context, string, models.SObject) error {
	return nil
}
func (noopRollupProcessor) ProcessRollups(context.Context, string, models.SObject) error { return nil }
func (noopRollupProcessor) AffectedParentRows(context.Context, string, models.SObject) ([]recordlocks.ParentKey, error) {
	return nil, nil
}
func (noopRollupProcessor) AffectedParentRowsForInsert(context.Context, string, models.SObject) ([]recordlocks.ParentKey, error) {
	return nil, nil
}
func (noopRollupProcessor) AffectedParentRowsForUpdate(context.Context, string, models.SObject, models.SObject) ([]recordlocks.ParentKey, error) {
	return nil, nil
}
func (noopRollupProcessor) ProcessRollupsForUpdate(context.Context, string, models.SObject, models.SObject) error {
	return nil
}
func (noopRollupProcessor) StageRollupCountUpdatePreimage(ctx context.Context, _ string, _ models.SObject) (context.Context, error) {
	return ctx, nil
}
func (noopRollupProcessor) ProcessBulkRollups(context.Context, string, []models.SObject, []bool) error {
	return nil
}
func (noopRollupProcessor) RecalculateAll(context.Context, string, ports.RollupRecalculateBounds) (ports.RollupRecalculateProgress, error) {
	return ports.RollupRecalculateProgress{Complete: true}, nil
}

type mutatingRecordEventBus struct {
	mutate func(sharedEvents.EventType, models.SObject)
}

func (b mutatingRecordEventBus) Subscribe(_ sharedEvents.EventType, _ ports.EventHandler) func() {
	return func() {}
}

func (b mutatingRecordEventBus) Publish(_ context.Context, eventType sharedEvents.EventType, payload interface{}) error {
	recordPayload, ok := payload.(eventFeature.RecordEventPayload)
	if ok && b.mutate != nil {
		b.mutate(eventType, recordPayload.Record)
	}
	return nil
}

func (b mutatingRecordEventBus) PublishAsyncWithContext(_ context.Context, _ sharedEvents.EventType, _ interface{}) {
}

type captureRecordRepo struct {
	stubUpsertRecordRepo
	old           models.SObject
	exists        map[string]map[string]bool
	recordTypes   map[string]string
	recordTypeFn  func(ctx context.Context, id string) (string, error)
	findOneFn     func(ctx context.Context, tableName string, id string) (models.SObject, error)
	getChildrenFn func(ctx context.Context, childTable string, foreignKey string, parentID string) ([]models.SObject, error)
	inserted      models.SObject
	bulkInserted  []models.SObject
	bulkLanding   []bool
	updated       models.SObject
	deletedID     string
}

func (r *captureRecordRepo) Exists(ctx context.Context, tableName string, id string) (bool, error) {
	if r.exists == nil || r.exists[tableName] == nil {
		return false, nil
	}
	return r.exists[tableName][id], nil
}

func (r *captureRecordRepo) Insert(ctx context.Context, tableName string, record models.SObject) error {
	r.inserted = cloneSObject(record)
	return nil
}

func (r *captureRecordRepo) BulkInsert(ctx context.Context, tableName string, records []models.SObject, cols []string, opts interface{}) error {
	r.bulkInserted = cloneSObjects(records)
	return nil
}

func (r *captureRecordRepo) BulkInsertWithLanding(ctx context.Context, t string, recs []models.SObject, cols []string, opts interface{}) ([]bool, error) {
	r.bulkInserted = cloneSObjects(recs)
	if r.bulkLanding != nil {
		return append([]bool(nil), r.bulkLanding...), nil
	}
	landed := make([]bool, len(recs))
	for i := range landed {
		landed[i] = true
	}
	return landed, nil
}

func (r *captureRecordRepo) GetLock(ctx context.Context, tableName string, id string) (models.SObject, error) {
	return cloneSObject(r.old), nil
}

func (r *captureRecordRepo) FindOne(ctx context.Context, tableName string, id string) (models.SObject, error) {
	if r.findOneFn != nil {
		return r.findOneFn(ctx, tableName, id)
	}
	if r.inserted != nil && r.inserted[constants.FieldID] == id {
		return cloneSObject(r.inserted), nil
	}
	for _, rec := range r.bulkInserted {
		if rec[constants.FieldID] == id {
			return cloneSObject(rec), nil
		}
	}
	return cloneSObject(r.old), nil
}

func (r *captureRecordRepo) FindMany(ctx context.Context, tableName string, ids []string) (map[string]models.SObject, error) {
	out := make(map[string]models.SObject, len(ids))
	for _, id := range ids {
		if r.inserted != nil && r.inserted[constants.FieldID] == id {
			out[id] = cloneSObject(r.inserted)
			continue
		}
		for _, rec := range r.bulkInserted {
			if rec[constants.FieldID] == id {
				out[id] = cloneSObject(rec)
				break
			}
		}
	}
	return out, nil
}

func (r *captureRecordRepo) GetChildren(ctx context.Context, childTable string, foreignKey string, parentID string) ([]models.SObject, error) {
	if r.getChildrenFn != nil {
		return r.getChildrenFn(ctx, childTable, foreignKey, parentID)
	}
	return nil, nil
}

func (r *captureRecordRepo) GetRecordType(ctx context.Context, id string) (string, error) {
	if r.recordTypeFn != nil {
		return r.recordTypeFn(ctx, id)
	}
	if r.recordTypes == nil {
		return "", nil
	}
	return r.recordTypes[id], nil
}

func (r *captureRecordRepo) Update(ctx context.Context, tableName string, id string, updates models.SObject) error {
	r.updated = cloneSObject(updates)
	return nil
}

func (r *captureRecordRepo) Delete(ctx context.Context, tableName string, id string) error {
	r.deletedID = id
	return nil
}

func newRecordEntrypointTestService(schema *models.ObjectMetadata, repo *captureRecordRepo) *PersistenceService {
	metaRepo := &stubUpsertMetaRepo{schema: schema}
	metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	ps := &PersistenceService{
		repo:                repo,
		rollup:              noopRollupProcessor{},
		metadata:            metaSvc,
		permissions:         permSvc,
		eventBus:            &eventFeature.NoOpEventBus{},
		validator:           metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator:     validation.NewSchemaValidator(),
		txManager:           inlineTxManager{},
		parentLaneAdmission: &fakeParentLaneAdmissionController{},
		cacheInvalidations:  newRecordCacheInvalidations(),
		writeCanonicalizers: newRecordWriteCanonicalizers(),
	}
	ps.registerBuiltInSecurityCacheInvalidations()
	ps.registerBuiltInSecurityWriteCanonicalizers()
	ps.registerBuiltInSecurityWriteValidators()
	return installNoOpPermissionStateAuthority(ps)
}

func recordEntrypointUser() *models.UserSession {
	return &models.UserSession{ID: "005-user", ProfileID: constants.ProfileStandardUser}
}

func recordEntrypointSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		ID:           "obj_probe",
		APIName:      "ProbeObject",
		Label:        "Probe",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{
				APIName:       "PolyId",
				Type:          constants.FieldTypeReference,
				ReferenceTo:   []string{"User", "Group"},
				IsPolymorphic: true,
				Createable:    true,
				Updateable:    true,
			},
			{APIName: "PolyId_type", Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{
				APIName:               "ProtectedField",
				Type:                  constants.FieldTypeString,
				Createable:            false,
				Updateable:            false,
				WriteProtectionPolicy: constants.FieldWriteProtectionTrustedOnly,
			},
		},
	}
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

func requiredPicklistRecordEntrypointSchema() *models.ObjectMetadata {
	schema := recordEntrypointSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName:    "Stage__c",
		Type:       constants.FieldTypePicklist,
		Createable: true,
		Updateable: true,
		Required:   true,
		Options:    models.PicklistOptionsFromStrings([]string{"Open", "Closed"}),
	})
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

func recordEntrypointSchemaWithWritableReferenceTo(targets []string) *models.ObjectMetadata {
	schema := recordEntrypointSchema()
	for i := range schema.Fields {
		if schema.Fields[i].APIName == "PolyId" {
			schema.Fields[i].WritableReferenceTo = append([]string(nil), targets...)
		}
	}
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

func groupUserRoleEntrypointSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		ID:           "obj_group",
		APIName:      constants.TableGroup,
		Label:        "Group",
		SharingModel: constants.SharingModelPublicReadWrite,
		AccessPolicy: groupUserRoleWriteValidationAccessPolicy(),
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldGroup_Name, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{APIName: constants.FieldGroup_Label, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{APIName: constants.FieldGroup_Type, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{
				APIName:     constants.FieldGroup_UserRoleID,
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{constants.TableUserRole},
				Createable:  true,
				Updateable:  true,
			},
		},
	}
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

func TestInsertEntrypointDerivesPolymorphicDiscriminatorAndScrubsCallerType(t *testing.T) {
	repo := &captureRecordRepo{exists: map[string]map[string]bool{"User": {"005-target": true}}}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	_, err := ps.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"PolyId":            "005-target",
		"PolyId_type":       "Group",
	}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Insert: %v", err)
	}
	if repo.inserted["PolyId_type"] != "User" {
		t.Fatalf("inserted PolyId_type = %v, want User; record=%#v", repo.inserted["PolyId_type"], repo.inserted)
	}
}

func TestInsertEntrypointAcceptsGroupPolymorphicTargetAndScrubsCallerType(t *testing.T) {
	repo := &captureRecordRepo{exists: map[string]map[string]bool{"Group": {"00G-target": true}}}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	_, err := ps.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"PolyId":            "00G-target",
		"PolyId_type":       "User",
	}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Insert with Group target: %v", err)
	}
	if repo.inserted["PolyId_type"] != "Group" {
		t.Fatalf("inserted PolyId_type = %v, want Group; record=%#v", repo.inserted["PolyId_type"], repo.inserted)
	}
}

func TestInsertEntrypointRejectsMissingPolymorphicTarget(t *testing.T) {
	repo := &captureRecordRepo{}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	_, err := ps.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"PolyId":            "00G-missing",
	}, recordEntrypointUser())
	if err == nil {
		t.Fatal("Insert accepted missing polymorphic target")
	}
	if !strings.Contains(err.Error(), "not found in any of the allowed objects") {
		t.Fatalf("error = %v, want missing polymorphic target rejection", err)
	}
	if repo.inserted != nil {
		t.Fatalf("missing polymorphic target was persisted: %#v", repo.inserted)
	}
}

func TestInsertEntrypointRejectsPolymorphicTargetOutsideWritableReferenceToForUserContext(t *testing.T) {
	repo := &captureRecordRepo{exists: map[string]map[string]bool{"Group": {"00G-target": true}}}
	ps := newRecordEntrypointTestService(recordEntrypointSchemaWithWritableReferenceTo([]string{"User"}), repo)

	_, err := ps.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"PolyId":            "00G-target",
	}, recordEntrypointUser())
	if err == nil {
		t.Fatal("Insert accepted Group target outside WritableReferenceTo")
	}

	trustedCtx := constants.WithSecurityContext(context.Background(), constants.ServiceContext, "trusted reference-target write test")
	if _, err := ps.Insert(trustedCtx, "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"PolyId":            "00G-target",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("trusted Insert with Group target: %v", err)
	}
	if repo.inserted["PolyId_type"] != "Group" {
		t.Fatalf("trusted inserted PolyId_type = %v, want Group; record=%#v", repo.inserted["PolyId_type"], repo.inserted)
	}
}

func TestBulkInsertEntrypointDerivesPolymorphicDiscriminatorAndScrubsCallerType(t *testing.T) {
	repo := &captureRecordRepo{exists: map[string]map[string]bool{"User": {"005-target": true}}}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	result, err := ps.BulkInsert(context.Background(), "ProbeObject", []models.SObject{{
		constants.FieldName: "probe",
		"PolyId":            "005-target",
		"PolyId_type":       "Group",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v (result=%+v)", err, result)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulk inserted rows = %d, want 1", len(repo.bulkInserted))
	}
	if repo.bulkInserted[0]["PolyId_type"] != "User" {
		t.Fatalf("bulk PolyId_type = %v, want User; record=%#v", repo.bulkInserted[0]["PolyId_type"], repo.bulkInserted[0])
	}
}

func TestBulkInsertEntrypointAcceptsGroupPolymorphicTargetAndScrubsCallerType(t *testing.T) {
	repo := &captureRecordRepo{exists: map[string]map[string]bool{"Group": {"00G-target": true}}}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	result, err := ps.BulkInsert(context.Background(), "ProbeObject", []models.SObject{{
		constants.FieldName: "probe",
		"PolyId":            "00G-target",
		"PolyId_type":       "User",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert with Group target: %v (result=%+v)", err, result)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulk inserted rows = %d, want 1", len(repo.bulkInserted))
	}
	if repo.bulkInserted[0]["PolyId_type"] != "Group" {
		t.Fatalf("bulk PolyId_type = %v, want Group; record=%#v", repo.bulkInserted[0]["PolyId_type"], repo.bulkInserted[0])
	}
}

func TestUpdateEntrypointDerivesAndClearsPolymorphicDiscriminator(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:   "a00-row",
			constants.FieldName: "old",
			"PolyId":            "00G-group",
			"PolyId_type":       "Group",
		},
		exists: map[string]map[string]bool{"User": {"005-target": true}},
	}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	if err := ps.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
		"PolyId":      "005-target",
		"PolyId_type": "Group",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("Update changed lookup: %v", err)
	}
	if repo.updated["PolyId_type"] != "User" {
		t.Fatalf("updated PolyId_type = %v, want User; updates=%#v", repo.updated["PolyId_type"], repo.updated)
	}

	repo.updated = nil
	if err := ps.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
		"PolyId":      nil,
		"PolyId_type": "Group",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("Update cleared lookup: %v", err)
	}
	if val, ok := repo.updated["PolyId_type"]; !ok || val != nil {
		t.Fatalf("cleared PolyId_type = %#v (present=%v), want nil", val, ok)
	}
}

func TestUpdateEntrypointAcceptsGroupPolymorphicTargetAndScrubsCallerType(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:   "a00-row",
			constants.FieldName: "old",
			"PolyId":            "005-user",
			"PolyId_type":       "User",
		},
		exists: map[string]map[string]bool{"Group": {"00G-target": true}},
	}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	if err := ps.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
		"PolyId":      "00G-target",
		"PolyId_type": "User",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("Update with Group target: %v", err)
	}
	if repo.updated["PolyId_type"] != "Group" {
		t.Fatalf("updated PolyId_type = %v, want Group; updates=%#v", repo.updated["PolyId_type"], repo.updated)
	}
}

func TestPreviewAndCommitPreparedUpdateScrubPolymorphicDiscriminatorEntrypoints(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:   "a00-row",
			constants.FieldName: "old",
			"PolyId":            "00G-group",
			"PolyId_type":       "Group",
		},
		exists: map[string]map[string]bool{"User": {"005-target": true}},
	}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	prepared, err := ps.PreviewBeforeUpdate(context.Background(), "ProbeObject", "a00-row", models.SObject{
		"PolyId":      "005-target",
		"PolyId_type": "Group",
	}, models.SObject{"PolyId_type": "Group"}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("PreviewBeforeUpdate: %v", err)
	}
	if prepared.EffectiveUpdates["PolyId_type"] != "User" {
		t.Fatalf("preview PolyId_type = %v, want User; prepared=%#v", prepared.EffectiveUpdates["PolyId_type"], prepared.EffectiveUpdates)
	}

	prepared.InternalUpdates = models.SObject{"PolyId_type": "Group"}
	if err := ps.CommitPreparedUpdate(context.Background(), "ProbeObject", "a00-row", prepared, models.SObject{"PolyId_type": "Group"}, recordEntrypointUser()); err != nil {
		t.Fatalf("CommitPreparedUpdate: %v", err)
	}
	if repo.updated["PolyId_type"] != "User" {
		t.Fatalf("commit PolyId_type = %v, want User; updates=%#v", repo.updated["PolyId_type"], repo.updated)
	}
}

func TestPreviewAndCommitPreparedUpdateAcceptGroupPolymorphicTarget(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:   "a00-row",
			constants.FieldName: "old",
			"PolyId":            "005-user",
			"PolyId_type":       "User",
		},
		exists: map[string]map[string]bool{"Group": {"00G-target": true}},
	}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	prepared, err := ps.PreviewBeforeUpdate(context.Background(), "ProbeObject", "a00-row", models.SObject{
		"PolyId":      "00G-target",
		"PolyId_type": "User",
	}, models.SObject{"PolyId_type": "User"}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("PreviewBeforeUpdate with Group target: %v", err)
	}
	if prepared.EffectiveUpdates["PolyId_type"] != "Group" {
		t.Fatalf("preview PolyId_type = %v, want Group; prepared=%#v", prepared.EffectiveUpdates["PolyId_type"], prepared.EffectiveUpdates)
	}

	prepared.InternalUpdates = models.SObject{"PolyId_type": "User"}
	if err := ps.CommitPreparedUpdate(context.Background(), "ProbeObject", "a00-row", prepared, models.SObject{"PolyId_type": "User"}, recordEntrypointUser()); err != nil {
		t.Fatalf("CommitPreparedUpdate with Group target: %v", err)
	}
	if repo.updated["PolyId_type"] != "Group" {
		t.Fatalf("commit PolyId_type = %v, want Group; updates=%#v", repo.updated["PolyId_type"], repo.updated)
	}
}

func TestPreviewAndCommitPreparedUpdateNormalizesBlankGroupUserRoleClear(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:               "00G-row",
			constants.FieldGroup_Name:       "Queue Group",
			constants.FieldGroup_Label:      "Queue Group",
			constants.FieldGroup_Type:       "Queue",
			constants.FieldGroup_UserRoleID: "00E-role",
		},
	}
	ps := newRecordEntrypointTestService(groupUserRoleEntrypointSchema(), repo)

	prepared, err := ps.PreviewBeforeUpdate(context.Background(), constants.TableGroup, "00G-row", models.SObject{
		constants.FieldGroup_UserRoleID: "   ",
	}, models.SObject{}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("PreviewBeforeUpdate blank Group.UserRoleId clear: %v", err)
	}
	if val, ok := prepared.Record[constants.FieldGroup_UserRoleID]; !ok || val != nil {
		t.Fatalf("prepared.Record UserRoleId = %#v (present=%v), want nil", val, ok)
	}
	if val, ok := prepared.EffectiveUpdates[constants.FieldGroup_UserRoleID]; !ok || val != nil {
		t.Fatalf("prepared.EffectiveUpdates UserRoleId = %#v (present=%v), want nil", val, ok)
	}

	forcedCtx := withParentLockForcedPrelocks(context.Background(), []recordlocks.ParentKey{{
		ObjectName: constants.TableUserRole,
		RecordID:   "00E-role",
	}})
	err = ps.RunInPessimisticTransaction(forcedCtx, func(txCtx context.Context) error {
		txCtx = context.WithValue(txCtx, groupMembershipClosureTxStateKey{}, &groupMembershipClosureTxState{})
		return ps.CommitPreparedUpdate(txCtx, constants.TableGroup, "00G-row", prepared, models.SObject{}, recordEntrypointUser())
	})
	if err != nil {
		t.Fatalf("CommitPreparedUpdate blank Group.UserRoleId clear: %v", err)
	}
	if val, ok := repo.updated[constants.FieldGroup_UserRoleID]; !ok || val != nil {
		t.Fatalf("committed UserRoleId = %#v (present=%v), want nil", val, ok)
	}
}

func TestUpdateEntrypointNormalizesBlankGroupUserRoleClear(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:               "00G-row",
			constants.FieldGroup_Name:       "Queue Group",
			constants.FieldGroup_Label:      "Queue Group",
			constants.FieldGroup_Type:       "Queue",
			constants.FieldGroup_UserRoleID: "00E-role",
		},
	}
	ps := newRecordEntrypointTestService(groupUserRoleEntrypointSchema(), repo)

	forcedCtx := withParentLockForcedPrelocks(context.Background(), []recordlocks.ParentKey{{
		ObjectName: constants.TableUserRole,
		RecordID:   "00E-role",
	}})
	err := ps.RunInPessimisticTransaction(forcedCtx, func(txCtx context.Context) error {
		txCtx = context.WithValue(txCtx, groupMembershipClosureTxStateKey{}, &groupMembershipClosureTxState{})
		return ps.Update(txCtx, constants.TableGroup, "00G-row", models.SObject{
			constants.FieldGroup_UserRoleID: "   ",
		}, recordEntrypointUser())
	})
	if err != nil {
		t.Fatalf("Update blank Group.UserRoleId clear: %v", err)
	}
	if val, ok := repo.updated[constants.FieldGroup_UserRoleID]; !ok || val != nil {
		t.Fatalf("updated UserRoleId = %#v (present=%v), want nil", val, ok)
	}
}

func TestBeforeSaveFlowPolymorphicGroupTargetRevalidatedAndStamped(t *testing.T) {
	insertRepo := &captureRecordRepo{exists: map[string]map[string]bool{"Group": {"00G-flow": true}}}
	insertPS := newRecordEntrypointTestService(recordEntrypointSchema(), insertRepo)
	insertPS.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeCreate {
			record["PolyId"] = "00G-flow"
			record["PolyId_type"] = "User"
		}
	}}

	if _, err := insertPS.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("Insert with before-save Group target: %v", err)
	}
	if insertRepo.inserted["PolyId_type"] != "Group" {
		t.Fatalf("before-save insert PolyId_type = %v, want Group; record=%#v", insertRepo.inserted["PolyId_type"], insertRepo.inserted)
	}

	updateRepo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:   "a00-row",
			constants.FieldName: "old",
			"PolyId":            "005-user",
			"PolyId_type":       "User",
		},
		exists: map[string]map[string]bool{"Group": {"00G-flow": true}},
	}
	updatePS := newRecordEntrypointTestService(recordEntrypointSchema(), updateRepo)
	updatePS.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeUpdate {
			record["PolyId"] = "00G-flow"
			record["PolyId_type"] = "User"
		}
	}}

	if err := updatePS.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
		constants.FieldName: "new",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("Update with before-save Group target: %v", err)
	}
	if updateRepo.updated["PolyId_type"] != "Group" {
		t.Fatalf("before-save update PolyId_type = %v, want Group; updates=%#v", updateRepo.updated["PolyId_type"], updateRepo.updated)
	}
}

func TestBulkInsertBeforeSaveFlowPolymorphicTargetValidatedAfterMutation(t *testing.T) {
	repo := &captureRecordRepo{exists: map[string]map[string]bool{"Group": {"00G-flow": true}}}
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), repo)

	var beforeCreateCount int
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType != sharedEvents.RecordBeforeCreate {
			return
		}
		beforeCreateCount++
		record["PolyId"] = "00G-flow"
		record["PolyId_type"] = "User"
	}}

	result, err := ps.BulkInsert(context.Background(), "ProbeObject", []models.SObject{{
		constants.FieldName: "probe",
		"PolyId":            "005-preflow-missing",
		"PolyId_type":       "User",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert with before-save polymorphic target correction: %v (result=%+v)", err, result)
	}
	if result.SuccessCount != 1 || result.FailedCount != 0 {
		t.Fatalf("BulkInsert result = %+v, want one success", result)
	}
	if beforeCreateCount != 1 {
		t.Fatalf("RecordBeforeCreate count = %d, want 1", beforeCreateCount)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulk inserted rows = %d, want 1", len(repo.bulkInserted))
	}
	if repo.bulkInserted[0]["PolyId"] != "00G-flow" {
		t.Fatalf("bulk PolyId = %v, want before-save target; record=%#v", repo.bulkInserted[0]["PolyId"], repo.bulkInserted[0])
	}
	if repo.bulkInserted[0]["PolyId_type"] != "Group" {
		t.Fatalf("bulk PolyId_type = %v, want Group after post-before-save validation; record=%#v", repo.bulkInserted[0]["PolyId_type"], repo.bulkInserted[0])
	}
}

func TestBulkInsertCallerLookupValidationRunsBeforeBeforeSaveFlow(t *testing.T) {
	schema := recordEntrypointSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName:     "ParentId",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"ParentObject"},
		Createable:  true,
		Updateable:  true,
	})
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}

	repo := &captureRecordRepo{
		findOneFn: func(_ context.Context, tableName string, id string) (models.SObject, error) {
			if tableName == "ParentObject" && id == "parent-flow" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, nil
		},
	}
	ps := newRecordEntrypointTestService(schema, repo)

	var beforeCreateCount int
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType != sharedEvents.RecordBeforeCreate {
			return
		}
		beforeCreateCount++
		record["ParentId"] = "parent-flow"
	}}

	result, err := ps.BulkInsert(context.Background(), "ProbeObject", []models.SObject{{
		constants.FieldName: "probe",
		"ParentId":          "missing-parent",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err == nil {
		t.Fatal("BulkInsert err = nil; want caller lookup validation failure before before-save flow")
	}
	if result.SuccessCount != 0 || result.FailedCount != 1 {
		t.Fatalf("BulkInsert result = %+v; want one caller lookup failure", result)
	}
	if beforeCreateCount != 0 {
		t.Fatalf("RecordBeforeCreate count = %d; want caller lookup validation to reject before flow", beforeCreateCount)
	}
	if len(result.Errors) != 1 ||
		!strings.Contains(result.Errors[0], `ParentId: referenced ParentObject record "missing-parent" is not accessible or does not exist`) {
		t.Fatalf("result.Errors = %#v; want ParentId lookup validation error", result.Errors)
	}
	if len(repo.bulkInserted) != 0 {
		t.Fatalf("bulkInserted = %#v; want no DML after caller lookup validation failure", repo.bulkInserted)
	}
}

func TestWriteProtectedFieldEntrypointsRejectUserWritesAndAllowTrustedWrites(t *testing.T) {
	ps := newRecordEntrypointTestService(recordEntrypointSchema(), &captureRecordRepo{})

	if _, err := ps.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"ProtectedField":    "spoof",
	}, recordEntrypointUser()); err == nil {
		t.Fatal("Insert with protected field succeeded, want validation error")
	}

	if _, err := ps.BulkInsert(context.Background(), "ProbeObject", []models.SObject{{
		constants.FieldName: "probe",
		"ProtectedField":    "spoof",
	}}, recordEntrypointUser(), BulkInsertOptions{}); err == nil {
		t.Fatal("BulkInsert with protected field succeeded, want validation error")
	}

	if err := ps.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
		"ProtectedField": "spoof",
	}, recordEntrypointUser()); err == nil {
		t.Fatal("Update with protected field succeeded, want validation error")
	}

	trustedRepo := &captureRecordRepo{}
	trustedPS := newRecordEntrypointTestService(recordEntrypointSchema(), trustedRepo)
	trustedCtx := constants.WithSecurityContext(context.Background(), constants.ServiceContext, "trusted test writer")
	if _, err := trustedPS.Insert(trustedCtx, "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"ProtectedField":    "system",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("trusted Insert with protected field: %v", err)
	}
	if trustedRepo.inserted["ProtectedField"] != "system" {
		t.Fatalf("trusted inserted ProtectedField = %v, want system; record=%#v", trustedRepo.inserted["ProtectedField"], trustedRepo.inserted)
	}

	trustedRepo = &captureRecordRepo{old: models.SObject{
		constants.FieldID:   "a00-row",
		constants.FieldName: "probe",
		"ProtectedField":    "old",
	}}
	trustedPS = newRecordEntrypointTestService(recordEntrypointSchema(), trustedRepo)
	if err := trustedPS.Update(trustedCtx, "ProbeObject", "a00-row", models.SObject{
		"ProtectedField": "system",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("trusted Update with protected field: %v", err)
	}
	if trustedRepo.updated["ProtectedField"] != "system" {
		t.Fatalf("trusted updated ProtectedField = %v, want system; updates=%#v", trustedRepo.updated["ProtectedField"], trustedRepo.updated)
	}
}

func TestAuthorityOnlyFieldRejectsSystemContextBeforeBeforeSave(t *testing.T) {
	schema := recordEntrypointSchema()
	for i := range schema.Fields {
		if schema.Fields[i].APIName == "ProtectedField" {
			schema.Fields[i].WriteProtectionPolicy = constants.FieldWriteProtectionAuthorityOnly
		}
	}

	repo := &captureRecordRepo{}
	ps := newRecordEntrypointTestService(schema, repo)
	beforeSaveCalls := 0
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, _ models.SObject) {
		if eventType == sharedEvents.RecordBeforeCreate {
			beforeSaveCalls++
		}
	}}
	systemCtx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "authority-only admission test")

	_, err := ps.Insert(systemCtx, "ProbeObject", models.SObject{
		constants.FieldName: "probe",
		"ProtectedField":    "system",
	}, recordEntrypointUser())
	if err == nil {
		t.Fatal("SystemContext Insert accepted an authority-only field")
	}
	if !strings.Contains(err.Error(), constants.FieldWriteProtectionAuthorityOnly) {
		t.Fatalf("Insert error = %v, want authority-only policy", err)
	}
	if beforeSaveCalls != 0 {
		t.Fatalf("before-save calls = %d, want rejection before automation", beforeSaveCalls)
	}
	if repo.inserted != nil {
		t.Fatalf("Insert reached repository: %#v", repo.inserted)
	}
}

func TestWriteProtectedFieldEntrypointsRejectBeforeSaveFlowWrites(t *testing.T) {
	insertRepo := &captureRecordRepo{}
	insertPS := newRecordEntrypointTestService(recordEntrypointSchema(), insertRepo)
	insertPS.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeCreate {
			record["ProtectedField"] = "flow"
		}
	}}

	if _, err := insertPS.Insert(context.Background(), "ProbeObject", models.SObject{
		constants.FieldName: "probe",
	}, recordEntrypointUser()); err == nil {
		t.Fatal("Insert with before-save protected-field mutation succeeded, want validation error")
	}
	if insertRepo.inserted != nil {
		t.Fatalf("Insert persisted after protected-field mutation: %#v", insertRepo.inserted)
	}

	updateRepo := &captureRecordRepo{old: models.SObject{
		constants.FieldID:   "a00-row",
		constants.FieldName: "old",
		"ProtectedField":    "old",
	}}
	updatePS := newRecordEntrypointTestService(recordEntrypointSchema(), updateRepo)
	updatePS.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeUpdate {
			record["ProtectedField"] = "flow"
		}
	}}

	if err := updatePS.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
		constants.FieldName: "new",
	}, recordEntrypointUser()); err == nil {
		t.Fatal("Update with before-save protected-field mutation succeeded, want validation error")
	}
	if updateRepo.updated != nil {
		t.Fatalf("Update persisted after protected-field mutation: %#v", updateRepo.updated)
	}
}

func TestRequiredPicklistUpdateEntrypointsRejectBeforeSaveBlankMutation(t *testing.T) {
	tests := []struct {
		name          string
		mutationField string
		run           func(*PersistenceService) error
	}{
		{
			name:          "Update canonical field",
			mutationField: "Stage__c",
			run: func(ps *PersistenceService) error {
				return ps.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
					constants.FieldName: "new",
				}, recordEntrypointUser())
			},
		},
		{
			name:          "Update case-variant field",
			mutationField: "stage__c",
			run: func(ps *PersistenceService) error {
				return ps.Update(context.Background(), "ProbeObject", "a00-row", models.SObject{
					constants.FieldName: "new",
				}, recordEntrypointUser())
			},
		},
		{
			name:          "PreviewBeforeUpdate canonical field",
			mutationField: "Stage__c",
			run: func(ps *PersistenceService) error {
				_, err := ps.PreviewBeforeUpdate(context.Background(), "ProbeObject", "a00-row", models.SObject{
					constants.FieldName: "new",
				}, nil, recordEntrypointUser())
				return err
			},
		},
		{
			name:          "PreviewBeforeUpdate case-variant field",
			mutationField: "stage__c",
			run: func(ps *PersistenceService) error {
				_, err := ps.PreviewBeforeUpdate(context.Background(), "ProbeObject", "a00-row", models.SObject{
					constants.FieldName: "new",
				}, nil, recordEntrypointUser())
				return err
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repo := &captureRecordRepo{old: models.SObject{
				constants.FieldID:   "a00-row",
				constants.FieldName: "old",
				"Stage__c":          "Open",
			}}
			ps := newRecordEntrypointTestService(requiredPicklistRecordEntrypointSchema(), repo)
			mutationCount := 0
			ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
				if eventType == sharedEvents.RecordBeforeUpdate {
					mutationCount++
					record[tt.mutationField] = ""
				}
			}}

			err := tt.run(ps)
			if err == nil {
				t.Fatal("before-save blank required picklist mutation succeeded, want validation error")
			}
			if !strings.Contains(err.Error(), "Stage__c") {
				t.Fatalf("error = %v, want Stage__c", err)
			}
			if !strings.Contains(err.Error(), "is required") {
				t.Fatalf("error = %v, want required-field rejection", err)
			}
			if mutationCount != 1 {
				t.Fatalf("before-save mutation count = %d, want 1", mutationCount)
			}
			if repo.updated != nil {
				t.Fatalf("update persisted after before-save blank required picklist mutation: %#v", repo.updated)
			}
		})
	}
}

func objectPermissionsEntrypointSchema() *models.ObjectMetadata {
	return &models.ObjectMetadata{
		APIName: constants.TableObjectPermissions,
		AccessPolicy: &models.ObjectAccessPolicy{
			WriteValidation: &models.RecordWriteValidationAccessPolicy{
				Validators: append(
					append([]models.RecordWriteValidationPolicy{
						{Phase: string(contract.RecordWriteValidationBeforeCreate), Capability: securityWriteValidateObjectPermissionsCapability},
						{Phase: string(contract.RecordWriteValidationBeforeUpdate), Capability: securityWriteValidateObjectPermissionsCapability},
					}, systemProfileOwnedImmutableWriteValidationPolicies()...),
					managedPermissionSetDefinitionWriteValidationPolicies()...,
				),
				OwnerResolution: &models.OwnerResolutionSpec{
					Paths: []models.OwnerResolutionPath{
						{
							Hops: []models.OwnerResolutionHop{
								{ViaField: "ParentId", ToObject: constants.TablePermissionSet, Gate: &models.OwnerPredicate{Field: constants.FieldPermissionSet_IsOwnedByProfile, Is: true}},
								{ViaField: "ProfileId", ToObject: constants.TableProfile, SystemWhen: &models.OwnerPredicate{Field: constants.FieldProfile_IsSystem, Is: true}},
							},
						},
						{
							Hops: []models.OwnerResolutionHop{
								{ViaField: "ParentId", ToObject: constants.TableProfile, SystemWhen: &models.OwnerPredicate{Field: constants.FieldProfile_IsSystem, Is: true}},
							},
						},
					},
				},
			},
		},
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString},
			{APIName: constants.FieldObjectPermissions_ParentID, Type: constants.FieldTypeReference, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_SobjectType, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_PermissionsRead, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_PermissionsCreate, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_PermissionsEdit, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_PermissionsDelete, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_PermissionsViewAllRecords, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
			{APIName: constants.FieldObjectPermissions_PermissionsModifyAllRecords, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
		},
	}
}

func TestObjectPermissionsEntrypointsRejectBeforeSaveFlowContradictions(t *testing.T) {
	insertRepo := &captureRecordRepo{}
	insertPS := newRecordEntrypointTestService(objectPermissionsEntrypointSchema(), insertRepo)
	insertPS.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeCreate {
			record[constants.FieldObjectPermissions_PermissionsRead] = false
			record[constants.FieldObjectPermissions_PermissionsCreate] = true
		}
	}}

	if _, err := insertPS.Insert(context.Background(), constants.TableObjectPermissions, models.SObject{
		constants.FieldObjectPermissions_ParentID:        "0PS000000000001",
		constants.FieldObjectPermissions_SobjectType:     "Account",
		constants.FieldObjectPermissions_PermissionsRead: true,
	}, recordEntrypointUser()); err == nil {
		t.Fatal("Insert with before-save incoherent ObjectPermissions mutation succeeded, want validation error")
	}
	if insertRepo.inserted != nil {
		t.Fatalf("Insert persisted after before-save ObjectPermissions contradiction: %#v", insertRepo.inserted)
	}

	updateRepo := &captureRecordRepo{old: models.SObject{
		constants.FieldID:                                  "op-row",
		constants.FieldObjectPermissions_ParentID:          "0PS000000000001",
		constants.FieldObjectPermissions_SobjectType:       "Account",
		constants.FieldObjectPermissions_PermissionsRead:   true,
		constants.FieldObjectPermissions_PermissionsCreate: false,
	}}
	updatePS := newRecordEntrypointTestService(objectPermissionsEntrypointSchema(), updateRepo)
	updatePS.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeUpdate {
			record[constants.FieldObjectPermissions_PermissionsRead] = false
			record[constants.FieldObjectPermissions_PermissionsCreate] = true
		}
	}}

	if err := updatePS.Update(context.Background(), constants.TableObjectPermissions, "op-row", models.SObject{
		constants.FieldObjectPermissions_PermissionsRead: true,
	}, recordEntrypointUser()); err == nil {
		t.Fatal("Update with before-save incoherent ObjectPermissions mutation succeeded, want validation error")
	}
	if updateRepo.updated != nil {
		t.Fatalf("Update persisted after before-save ObjectPermissions contradiction: %#v", updateRepo.updated)
	}
}

func TestObjectPermissionsPreviewBeforeUpdateRejectsBeforeSaveFlowContradiction(t *testing.T) {
	repo := &captureRecordRepo{old: models.SObject{
		constants.FieldID:                                  "op-row",
		constants.FieldObjectPermissions_ParentID:          "0PS000000000001",
		constants.FieldObjectPermissions_SobjectType:       "Account",
		constants.FieldObjectPermissions_PermissionsRead:   true,
		constants.FieldObjectPermissions_PermissionsCreate: false,
	}}
	ps := newRecordEntrypointTestService(objectPermissionsEntrypointSchema(), repo)
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType == sharedEvents.RecordBeforeUpdate {
			record[constants.FieldObjectPermissions_PermissionsRead] = false
			record[constants.FieldObjectPermissions_PermissionsCreate] = true
		}
	}}

	if _, err := ps.PreviewBeforeUpdate(context.Background(), constants.TableObjectPermissions, "op-row", models.SObject{
		constants.FieldObjectPermissions_PermissionsRead: true,
	}, nil, recordEntrypointUser()); err == nil {
		t.Fatal("PreviewBeforeUpdate with before-save incoherent ObjectPermissions mutation succeeded, want validation error")
	}
}

func TestProcessWorkitemActorIdTrustedUpdatePersists(t *testing.T) {
	schema := processWorkitemEntrypointSchema()
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID: "04i-row",
			"ActorId":         "005-old",
			"ActorId_type":    "User",
		},
		exists: map[string]map[string]bool{"User": {"005-new": true}},
	}
	ps := newRecordEntrypointTestService(schema, repo)
	trustedCtx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "ApprovalService: update ProcessInstanceWorkitem")

	if err := ps.Update(trustedCtx, constants.TableProcessInstanceWorkitem, "04i-row", models.SObject{
		"ActorId": "005-new",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("trusted ProcessInstanceWorkitem.ActorId update: %v", err)
	}
	if repo.updated["ActorId"] != "005-new" {
		t.Fatalf("updated ActorId = %v, want 005-new; updates=%#v", repo.updated["ActorId"], repo.updated)
	}
}

func processWorkitemEntrypointSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		ID:           "obj_process_workitem",
		APIName:      constants.TableProcessInstanceWorkitem,
		Label:        "Approval Work Item",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{
				APIName:               "ActorId",
				Type:                  constants.FieldTypeReference,
				ReferenceTo:           []string{"User", "Group"},
				IsPolymorphic:         true,
				Createable:            false,
				Updateable:            false,
				WriteProtectionPolicy: constants.FieldWriteProtectionTrustedOnly,
			},
			{APIName: "ActorId_type", Type: constants.FieldTypeString, Createable: false, Updateable: false},
		},
	}
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

func cloneSObjects(in []models.SObject) []models.SObject {
	out := make([]models.SObject, len(in))
	for i := range in {
		out[i] = cloneSObject(in[i])
	}
	return out
}

var _ ports.TransactionManager = inlineTxManager{}
var _ ports.RollupProcessor = noopRollupProcessor{}
var _ ports.EventPublisher = mutatingRecordEventBus{}
