package records

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"testing"

	eventFeature "github.com/nexuscrm/events"
	"github.com/nexuscrm/kernel/pkg/apierr"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"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 relationshipDeleteRepo struct {
	stubUpsertRecordRepo
	records       map[string]models.SObject
	findRecords   map[string]models.SObject
	lockedRecords map[string]models.SObject
	childExists   bool
	children      map[string][]models.SObject
	childrenFn    func(childTable string, foreignKey string, parentID string, call int) ([]models.SObject, error)
	childCalls    map[string]int
	calls         []string
	updateCalls   []relationshipDeleteUpdate
	insertCalls   []relationshipDeleteUpdate
	physicalCalls []relationshipDeleteUpdate
}

type relationshipDeleteUpdate struct {
	table   string
	id      string
	updates models.SObject
}

func (r *relationshipDeleteRepo) FindOne(ctx context.Context, tableName string, id string) (models.SObject, error) {
	r.calls = append(r.calls, fmt.Sprintf("find:%s:%s", tableName, id))
	if r.findRecords != nil {
		return cloneSObject(r.findRecords[tableName]), nil
	}
	if r.records == nil {
		return nil, nil
	}
	if rec, ok := r.records[tableName]; ok {
		return cloneSObject(rec), nil
	}
	return nil, nil
}

func (r *relationshipDeleteRepo) FindAny(ctx context.Context, tableName string, id string) (models.SObject, error) {
	return r.FindOne(ctx, tableName, id)
}

func (r *relationshipDeleteRepo) GetLock(ctx context.Context, tableName string, id string) (models.SObject, error) {
	r.calls = append(r.calls, fmt.Sprintf("lock:%s:%s", tableName, id))
	if r.lockedRecords != nil {
		return cloneSObject(r.lockedRecords[tableName]), nil
	}
	if r.records == nil {
		return nil, nil
	}
	if rec, ok := r.records[tableName]; ok {
		return cloneSObject(rec), nil
	}
	return nil, nil
}

func (r *relationshipDeleteRepo) ExistsByField(ctx context.Context, tableName string, fieldName string, value interface{}) (bool, error) {
	r.calls = append(r.calls, fmt.Sprintf("exists:%s:%s:%v", tableName, fieldName, value))
	return r.childExists, nil
}

func (r *relationshipDeleteRepo) GetChildren(ctx context.Context, childTable string, foreignKey string, parentID string) ([]models.SObject, error) {
	r.calls = append(r.calls, fmt.Sprintf("children:%s:%s:%s", childTable, foreignKey, parentID))
	key := childTable + "|" + foreignKey + "|" + parentID
	if r.childCalls == nil {
		r.childCalls = map[string]int{}
	}
	r.childCalls[key]++
	if r.childrenFn != nil {
		children, err := r.childrenFn(childTable, foreignKey, parentID, r.childCalls[key])
		if err != nil {
			return nil, err
		}
		return cloneSObjects(children), nil
	}
	return cloneSObjects(r.children[key]), nil
}

func (r *relationshipDeleteRepo) Update(ctx context.Context, tableName string, id string, updates models.SObject) error {
	r.calls = append(r.calls, fmt.Sprintf("update:%s:%s", tableName, id))
	r.updateCalls = append(r.updateCalls, relationshipDeleteUpdate{table: tableName, id: id, updates: cloneSObject(updates)})
	return nil
}

func (r *relationshipDeleteRepo) Insert(ctx context.Context, tableName string, record models.SObject) error {
	r.calls = append(r.calls, fmt.Sprintf("insert:%s:%s", tableName, stringValue(record[constants.FieldID])))
	r.insertCalls = append(r.insertCalls, relationshipDeleteUpdate{table: tableName, id: stringValue(record[constants.FieldID]), updates: cloneSObject(record)})
	return nil
}

func (r *relationshipDeleteRepo) PhysicalDelete(ctx context.Context, tableName string, id string) error {
	r.calls = append(r.calls, fmt.Sprintf("physical:%s:%s", tableName, id))
	r.physicalCalls = append(r.physicalCalls, relationshipDeleteUpdate{table: tableName, id: id})
	return nil
}

type relationshipDeletePublishedEvent struct {
	eventType       sharedEvents.EventType
	objectAPIName   string
	securityContext constants.SecurityContext
}

type relationshipDeleteEventBus struct {
	events []relationshipDeletePublishedEvent
}

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

func (b *relationshipDeleteEventBus) Publish(ctx context.Context, eventType sharedEvents.EventType, payload interface{}) error {
	recordPayload, ok := payload.(eventFeature.RecordEventPayload)
	if ok {
		b.events = append(b.events, relationshipDeletePublishedEvent{
			eventType:       eventType,
			objectAPIName:   recordPayload.ObjectAPIName,
			securityContext: constants.GetSecurityContext(ctx),
		})
	}
	return nil
}

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

func (b *relationshipDeleteEventBus) saw(objectAPIName string, eventType sharedEvents.EventType) (relationshipDeletePublishedEvent, bool) {
	for _, event := range b.events {
		if event.eventType == eventType && event.objectAPIName == objectAPIName {
			return event, true
		}
	}
	return relationshipDeletePublishedEvent{}, false
}

// deleteSetNullRollupField builds a rollup-summary field on a secondary parent
// so that a set-null child's bystander lookup (relationshipField on childObject)
// genuinely feeds a parent rollup — making that parent a legitimate FOR UPDATE
// lock target under the narrowed lookup-lock predicate, which is what these
// set-null cascade lock-ordering/retry tests exercise.
func deleteSetNullRollupField(childObject, relationshipField string) models.FieldMetadata {
	return models.FieldMetadata{
		APIName:    "ChildCount__c",
		Type:       constants.FieldTypeRollupSummary,
		Createable: false,
		Updateable: false,
		RollupConfig: &models.RollupConfig{
			CalcType:          "COUNT",
			SummaryObject:     childObject,
			RelationshipField: relationshipField,
		},
	}
}

func relationshipDeleteParentSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Parent__c",
		Label:        "Parent",
		PluralLabel:  "Parents",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true, Createable: true, Updateable: true},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	makeFieldsMap(schema)
	return schema
}

func metadataDeployDeleteSchema(apiName string) *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      apiName,
		Label:        apiName,
		PluralLabel:  apiName,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true, Createable: true, Updateable: true},
		},
	}
	makeFieldsMap(schema)
	return schema
}

func relationshipDeleteChildSchema(field models.FieldMetadata) *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Child__c",
		Label:        "Child",
		PluralLabel:  "Children",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true, Createable: true, Updateable: true},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
			field,
		},
	}
	makeFieldsMap(schema)
	return schema
}

func relationshipDeleteExclusiveChildSchema(field models.FieldMetadata) *models.ObjectMetadata {
	schema := relationshipDeleteChildSchema(field)
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName:    "IsPrimary__c",
		Type:       constants.FieldTypeBoolean,
		Createable: true,
		Updateable: true,
	})
	schema.ExclusiveSiblingFields = []models.ExclusiveSiblingField{{
		FieldAPIName:          "IsPrimary__c",
		PartitionFieldAPIName: field.APIName,
	}}
	makeFieldsMap(schema)
	return schema
}

func TestDeleteWithOutcomesAuthorizedUsesLockedRecord(t *testing.T) {
	schema := relationshipDeleteParentSchema()
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{"Parent__c": {
			constants.FieldID: "parent-1", "State__c": "Open",
		}},
		lockedRecords: map[string]models.SObject{"Parent__c": {
			constants.FieldID: "parent-1", "State__c": "Locked",
		}},
	}
	ps := newRollupBoundaryTestService(repo, schema)
	sentinel := fmt.Errorf("locked policy denied")
	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "test locked delete authorization")
	err := ps.deleteWithOutcomesAuthorized(ctx, "Parent__c", "parent-1", recordEntrypointUser(), nil, nil, nil, func(_ context.Context, locked models.SObject) error {
		if got := locked["State__c"]; got != "Locked" {
			t.Fatalf("authorized row State__c = %#v, want locked image", got)
		}
		if relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1") < 0 {
			t.Fatalf("authorization ran without record lock: %#v", repo.calls)
		}
		return sentinel
	})
	if err != sentinel {
		t.Fatalf("Delete authorized error = %v, want %v", err, sentinel)
	}
	if len(repo.updateCalls) != 0 || len(repo.physicalCalls) != 0 {
		t.Fatalf("denied delete wrote update/physical rows: %#v / %#v", repo.updateCalls, repo.physicalCalls)
	}
}

func TestMetadataDeployDeleteHardDeletesOnlyTheRequestedRoot(t *testing.T) {
	root := metadataDeployDeleteSchema("MetadataRoot__c")
	repo := &relationshipDeleteRepo{records: map[string]models.SObject{
		"MetadataRoot__c": {
			constants.FieldID:   "root-1",
			constants.FieldName: "root",
		},
	}}
	ps := newRollupBoundaryTestService(repo, root)

	if err := ps.deleteMetadataDeployRecord(context.Background(), "MetadataRoot__c", "root-1", map[string]string{constants.FieldID: "root-1"}, recordEntrypointUser()); err != nil {
		t.Fatalf("deleteMetadataDeployRecord: %v", err)
	}
	if len(repo.physicalCalls) != 1 || repo.physicalCalls[0].table != "MetadataRoot__c" || repo.physicalCalls[0].id != "root-1" {
		t.Fatalf("physical deletes = %#v, want MetadataRoot__c/root-1", repo.physicalCalls)
	}
}

func TestMetadataDeployDeleteRequiresSchemaUniqueMatchField(t *testing.T) {
	root := metadataDeployDeleteSchema("MetadataRoot__c")
	root.Fields = append(root.Fields, models.FieldMetadata{APIName: "OwnerPack", Type: constants.FieldTypeString})
	makeFieldsMap(root)
	repo := &relationshipDeleteRepo{records: map[string]models.SObject{
		"MetadataRoot__c": {
			constants.FieldID:   "root-1",
			constants.FieldName: "root",
			"OwnerPack":         "",
		},
	}}
	ps := newRollupBoundaryTestService(repo, root)

	err := ps.deleteMetadataDeployRecord(context.Background(), "MetadataRoot__c", "root-1", map[string]string{"OwnerPack": ""}, recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "requires a unique match field") {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want unique-match rejection", err)
	}
	if len(repo.calls) != 0 {
		t.Fatalf("repository calls = %#v, want rejection before read or mutation", repo.calls)
	}
}

func TestMetadataDeployDeleteCascadeStillHonorsChildRestrictEdges(t *testing.T) {
	root := metadataDeployDeleteSchema("MetadataRoot__c")
	child := metadataDeployDeleteSchema("DefinitionChild__c")
	cascade := constants.DeleteRuleCascade
	child.Fields = append(child.Fields, models.FieldMetadata{
		APIName:     "MetadataRootId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"MetadataRoot__c"},
		DeleteRule:  &cascade,
		Createable:  true,
		Updateable:  true,
	})
	makeFieldsMap(child)
	runtimeDependent := metadataDeployDeleteSchema("RuntimeDependent__c")
	runtimeDependent.PluralLabel = "Runtime Dependents"
	runtimeDependent.Fields = append(runtimeDependent.Fields, models.FieldMetadata{
		APIName:     "DefinitionChildId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"DefinitionChild__c"},
	})
	makeFieldsMap(runtimeDependent)

	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"MetadataRoot__c": {
				constants.FieldID:   "root-1",
				constants.FieldName: "root",
			},
			"DefinitionChild__c": {
				constants.FieldID:   "child-1",
				constants.FieldName: "definition child",
			},
		},
		children: map[string][]models.SObject{
			"DefinitionChild__c|MetadataRootId__c|root-1": {{constants.FieldID: "child-1"}},
		},
		childExists: true,
	}
	ps := newRollupBoundaryTestService(repo, root, child, runtimeDependent)

	err := ps.deleteMetadataDeployRecord(context.Background(), "MetadataRoot__c", "root-1", map[string]string{constants.FieldID: "root-1"}, recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "referenced by Runtime Dependents") {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want child Restrict edge to block cascade", err)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("physical deletes = %#v, want child Restrict edge to block the entire transaction", repo.physicalCalls)
	}
}

func TestMetadataDeployDeleteFailsIfMatchDisappearsBeforeRootLock(t *testing.T) {
	root := metadataDeployDeleteSchema("MetadataRoot__c")
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{
			"MetadataRoot__c": {
				constants.FieldID:   "root-1",
				constants.FieldName: "root",
			},
		},
		lockedRecords: map[string]models.SObject{},
	}
	ps := newRollupBoundaryTestService(repo, root)

	err := ps.deleteMetadataDeployRecord(context.Background(), "MetadataRoot__c", "root-1", map[string]string{constants.FieldID: "root-1"}, recordEntrypointUser())
	if err == nil || !appErrors.IsNotFound(err) {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want required-match not found", err)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("physical deletes = %#v, want no mutation after lost match", repo.physicalCalls)
	}
}

func TestMetadataDeployDeleteRevalidatesMatchAfterRootLock(t *testing.T) {
	root := metadataDeployDeleteSchema("MetadataRoot__c")
	root.Fields = append(root.Fields, models.FieldMetadata{APIName: "DeveloperName", Type: constants.FieldTypeString, IsUnique: true})
	makeFieldsMap(root)
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{
			"MetadataRoot__c": {
				constants.FieldID:   "root-1",
				constants.FieldName: "root",
				"DeveloperName":     "Expected_Name",
			},
		},
		lockedRecords: map[string]models.SObject{
			"MetadataRoot__c": {
				constants.FieldID:   "root-1",
				constants.FieldName: "root",
				"DeveloperName":     "Changed_Name",
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, root)

	err := ps.deleteMetadataDeployRecord(context.Background(), "MetadataRoot__c", "root-1", map[string]string{
		"DeveloperName": "Expected_Name",
	}, recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "no longer matches metadata deploy delete") {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want locked match drift conflict", err)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("physical deletes = %#v, want no mutation after match drift", repo.physicalCalls)
	}
}

func TestMetadataDeployDeleteRevalidatesLivenessAfterRootLock(t *testing.T) {
	root := metadataDeployDeleteSchema("MetadataRoot__c")
	root.Fields = append(root.Fields, models.FieldMetadata{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true})
	makeFieldsMap(root)
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{
			"MetadataRoot__c": {
				constants.FieldID:        "root-1",
				constants.FieldName:      "root",
				constants.FieldIsDeleted: false,
			},
		},
		lockedRecords: map[string]models.SObject{
			"MetadataRoot__c": {
				constants.FieldID:        "root-1",
				constants.FieldName:      "root",
				constants.FieldIsDeleted: true,
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, root)

	err := ps.deleteMetadataDeployRecord(context.Background(), "MetadataRoot__c", "root-1", map[string]string{
		constants.FieldID: "root-1",
	}, recordEntrypointUser())
	if err == nil || !appErrors.IsNotFound(err) {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want tombstoned locked row treated as missing", err)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("physical deletes = %#v, want no mutation after concurrent soft delete", repo.physicalCalls)
	}
	if len(repo.insertCalls) != 0 {
		t.Fatalf("recycle-bin inserts = %#v, want existing tombstone left intact", repo.insertCalls)
	}
}

func TestMetadataDeployDeleteCascadesDefinitionOwnedChildren(t *testing.T) {
	for _, childObject := range []string{"ProcessNode", "ProcessAction"} {
		t.Run(childObject, func(t *testing.T) {
			root := metadataDeployDeleteSchema("ProcessDefinition")
			child := metadataDeployDeleteSchema(childObject)
			child.Fields = append(child.Fields,
				models.FieldMetadata{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
				models.FieldMetadata{
					APIName:     "ProcessDefinitionId",
					Type:        constants.FieldTypeReference,
					ReferenceTo: []string{"ProcessDefinition"},
					DeleteRule:  deleteRulePtr(constants.DeleteRuleCascade),
				},
			)
			makeFieldsMap(child)

			repo := &relationshipDeleteRepo{
				records: map[string]models.SObject{
					"ProcessDefinition": {
						constants.FieldID:   "definition-1",
						constants.FieldName: "definition",
					},
					childObject: {
						constants.FieldID:        "child-1",
						constants.FieldName:      "definition child",
						constants.FieldIsDeleted: false,
					},
				},
				children: map[string][]models.SObject{
					childObject + "|ProcessDefinitionId|definition-1": {{constants.FieldID: "child-1"}},
				},
			}
			ps := newRollupBoundaryTestService(repo, root, child)

			if err := ps.deleteMetadataDeployRecord(context.Background(), "ProcessDefinition", "definition-1", map[string]string{
				constants.FieldID: "definition-1",
			}, recordEntrypointUser()); err != nil {
				t.Fatalf("deleteMetadataDeployRecord: %v", err)
			}
			if len(repo.updateCalls) != 0 || len(repo.insertCalls) != 0 {
				t.Fatalf("definition child created soft-delete state: updates=%#v inserts=%#v", repo.updateCalls, repo.insertCalls)
			}
			if len(repo.physicalCalls) != 2 || repo.physicalCalls[0].table != childObject || repo.physicalCalls[1].table != "ProcessDefinition" {
				t.Fatalf("physical deletes = %#v, want definition child then ProcessDefinition root", repo.physicalCalls)
			}
		})
	}
}

func TestMetadataDeployDeleteCascadeRevalidatesChildLivenessAfterLock(t *testing.T) {
	root := metadataDeployDeleteSchema("ProcessDefinition")
	child := metadataDeployDeleteSchema("ProcessNode")
	child.Fields = append(child.Fields,
		models.FieldMetadata{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		models.FieldMetadata{
			APIName:     "ProcessDefinitionId",
			Type:        constants.FieldTypeReference,
			ReferenceTo: []string{"ProcessDefinition"},
			DeleteRule:  deleteRulePtr(constants.DeleteRuleCascade),
		},
	)
	makeFieldsMap(child)
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{
			"ProcessDefinition": {
				constants.FieldID:   "definition-1",
				constants.FieldName: "definition",
			},
			"ProcessNode": {
				constants.FieldID:        "node-1",
				constants.FieldName:      "node",
				constants.FieldIsDeleted: false,
			},
		},
		lockedRecords: map[string]models.SObject{
			"ProcessDefinition": {
				constants.FieldID:   "definition-1",
				constants.FieldName: "definition",
			},
			"ProcessNode": {
				constants.FieldID:        "node-1",
				constants.FieldName:      "node",
				constants.FieldIsDeleted: true,
			},
		},
		children: map[string][]models.SObject{
			"ProcessNode|ProcessDefinitionId|definition-1": {{constants.FieldID: "node-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, root, child)

	err := ps.deleteMetadataDeployRecord(context.Background(), "ProcessDefinition", "definition-1", map[string]string{
		constants.FieldID: "definition-1",
	}, recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "no longer live for metadata deploy delete") {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want locked cascade-child liveness conflict", err)
	}
	if len(repo.physicalCalls) != 0 || len(repo.insertCalls) != 0 {
		t.Fatalf("concurrently tombstoned child mutated: physical=%#v inserts=%#v", repo.physicalCalls, repo.insertCalls)
	}
}

func TestMetadataDeployDeleteStillRestrictsRuntimeDependents(t *testing.T) {
	root := metadataDeployDeleteSchema("ProcessDefinition")
	runtimeChild := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ProcessDefinitionId",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"ProcessDefinition"},
	})
	runtimeChild.APIName = "ProcessInstance"
	runtimeChild.Label = "Process Instance"
	runtimeChild.PluralLabel = "Process Instances"
	makeFieldsMap(runtimeChild)
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"ProcessDefinition": {
				constants.FieldID:   "definition-1",
				constants.FieldName: "definition",
			},
		},
		childExists: true,
	}
	ps := newRollupBoundaryTestService(repo, root, runtimeChild)

	err := ps.deleteMetadataDeployRecord(context.Background(), "ProcessDefinition", "definition-1", map[string]string{
		constants.FieldID: "definition-1",
	}, recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "referenced by Process Instances") {
		t.Fatalf("deleteMetadataDeployRecord error = %v, want live runtime-dependent conflict", err)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("physical deletes = %#v, want runtime dependent to block root", repo.physicalCalls)
	}
}

func TestDeleteProtectedRecordDecisionUsesLockedImage(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      constants.TableProfile,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString},
			{APIName: constants.FieldProfile_IsSystem, Type: constants.FieldTypeBoolean},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, IsSystem: true},
		},
	}
	makeFieldsMap(schema)
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{constants.TableProfile: {
			constants.FieldID: "profile-1", constants.FieldProfile_IsSystem: false,
		}},
		lockedRecords: map[string]models.SObject{constants.TableProfile: {
			constants.FieldID: "profile-1", constants.FieldProfile_IsSystem: true,
		}},
	}
	ps := newRollupBoundaryTestService(repo, schema)
	ctx := constants.WithSecurityContext(context.Background(), constants.ElevatedContext, "test locked protected-record decision")
	err := ps.Delete(ctx, constants.TableProfile, "profile-1", recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "CANNOT_DELETE_PROTECTED_RECORD") {
		t.Fatalf("Delete error = %v, want locked protected-record denial", err)
	}
	if len(repo.updateCalls) != 0 || len(repo.physicalCalls) != 0 {
		t.Fatalf("protected delete wrote update/physical rows: %#v / %#v", repo.updateCalls, repo.physicalCalls)
	}
}

func TestDeleteRechecksManagedOwnershipFromLockedImage(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      constants.TableEmailTemplate,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString},
			{APIName: constants.FieldSysEmailTemplate_OwnerPack, Type: constants.FieldTypeString},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean},
		},
	}
	makeFieldsMap(schema)
	repo := &relationshipDeleteRepo{
		findRecords: map[string]models.SObject{constants.TableEmailTemplate: {
			constants.FieldID: "template-1", constants.FieldSysEmailTemplate_OwnerPack: "",
		}},
		lockedRecords: map[string]models.SObject{constants.TableEmailTemplate: {
			constants.FieldID: "template-1", constants.FieldSysEmailTemplate_OwnerPack: "crm",
		}},
	}
	ps := newRollupBoundaryTestService(repo, schema)

	err := ps.Delete(context.Background(), constants.TableEmailTemplate, "template-1", recordEntrypointUser())
	if err == nil || !strings.Contains(err.Error(), "owned by pack") {
		t.Fatalf("Delete error = %v, want locked pack-ownership rejection", err)
	}
	if len(repo.updateCalls) != 0 || len(repo.physicalCalls) != 0 {
		t.Fatalf("managed delete wrote update/physical rows: %#v / %#v", repo.updateCalls, repo.physicalCalls)
	}
}

func TestDeleteRestrictChildCheckRunsAfterParentLock(t *testing.T) {
	restrict := constants.DeleteRuleRestrict
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &restrict,
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	lockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
	existsAt := relationshipDeleteCallIndex(repo.calls, "exists:Child__c:ParentId__c:parent-1")
	if lockAt < 0 {
		t.Fatalf("GetLock call missing: %#v", repo.calls)
	}
	if existsAt < 0 {
		t.Fatalf("restrict child check missing: %#v", repo.calls)
	}
	if existsAt < lockAt {
		t.Fatalf("restrict child check ran before parent lock: %#v", repo.calls)
	}
}

func TestCheckRestrictChildRelationshipsIgnoresNonRestrictEdges(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &cascade,
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{childExists: true}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.checkRestrictChildRelationships(context.Background(), parent, "Parent__c", "parent-1"); err != nil {
		t.Fatalf("checkRestrictChildRelationships: %v", err)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "exists:Child__c:ParentId__c:parent-1"); got >= 0 {
		t.Fatalf("cascade edge was probed by restrict-only check: %#v", repo.calls)
	}
}

func TestPhysicalDeleteRawBlocksHistoricalChildOnNonRestrictCoveredEdges(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	cases := []models.FieldMetadata{
		{APIName: "ParentId__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, DeleteRule: &cascade},
		{APIName: "MasterId__c", Type: constants.FieldTypeMasterDetail, ReferenceTo: []string{"Parent__c"}},
	}
	for _, field := range cases {
		t.Run(field.APIName, func(t *testing.T) {
			parent := relationshipDeleteParentSchema()
			child := relationshipDeleteChildSchema(field)
			repo := &relationshipDeleteRepo{
				records: map[string]models.SObject{
					"Parent__c": {
						constants.FieldID:        "parent-1",
						constants.FieldName:      "parent",
						constants.FieldIsDeleted: false,
					},
				},
				childExists: true,
			}
			ps := newRollupBoundaryTestService(repo, parent, child)

			err := ps.PhysicalDeleteRaw(context.Background(), "Parent__c", "parent-1")
			if err == nil {
				t.Fatal("PhysicalDeleteRaw succeeded with a live historical child on a non-Restrict covered edge")
			}
			apiErr, ok := err.(*apierr.Error)
			if !ok {
				t.Fatalf("error type = %T, want *apierr.Error: %v", err, err)
			}
			if apiErr.Status != http.StatusConflict {
				t.Fatalf("status = %d, want %d; err=%v", apiErr.Status, http.StatusConflict, err)
			}
			wantExists := "exists:Child__c:" + field.APIName + ":parent-1"
			lockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
			existsAt := relationshipDeleteCallIndex(repo.calls, wantExists)
			if lockAt < 0 {
				t.Fatalf("raw delete did not lock covered parent before scan: %#v", repo.calls)
			}
			if existsAt < 0 {
				t.Fatalf("raw delete did not probe covered child edge %q: %#v", wantExists, repo.calls)
			}
			if existsAt < lockAt {
				t.Fatalf("raw delete child scan ran before parent lock: %#v", repo.calls)
			}
			if got := relationshipDeleteCallIndex(repo.calls, "physical:Parent__c:parent-1"); got >= 0 {
				t.Fatalf("raw delete physically deleted after blocked child reference: %#v", repo.calls)
			}
		})
	}
}

func TestPhysicalDeleteRawBlocksRelationshipCascadeCoveredEdge(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	field := models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	}
	child := relationshipDeleteChildSchema(field)
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
		},
		childExists: true,
	}
	ps := newRollupBoundaryTestServiceWithMetadataRepo(repo, &stubUpsertMetaRepo{
		schemas: []*models.ObjectMetadata{parent, child},
		relationships: []*models.Relationship{{
			ChildObjectAPIName:  "Child__c",
			ParentObjectAPIName: "Parent__c",
			FieldAPIName:        "ParentId__c",
			CascadeDelete:       true,
		}},
	})

	err := ps.PhysicalDeleteRaw(context.Background(), "Parent__c", "parent-1")
	if err == nil {
		t.Fatal("PhysicalDeleteRaw succeeded with a live historical child on a relationship-cascade covered edge")
	}
	apiErr, ok := err.(*apierr.Error)
	if !ok {
		t.Fatalf("error type = %T, want *apierr.Error: %v", err, err)
	}
	if apiErr.Status != http.StatusConflict {
		t.Fatalf("status = %d, want %d; err=%v", apiErr.Status, http.StatusConflict, err)
	}
	wantExists := "exists:Child__c:ParentId__c:parent-1"
	if got := relationshipDeleteCallIndex(repo.calls, wantExists); got < 0 {
		t.Fatalf("raw delete did not probe relationship-cascade child edge %q: %#v", wantExists, repo.calls)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "physical:Parent__c:parent-1"); got >= 0 {
		t.Fatalf("raw delete physically deleted after blocked child reference: %#v", repo.calls)
	}
}

func TestPhysicalDeleteRawBlocksEventualCountRollupLookupChild(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	parent.Fields = append(parent.Fields, models.FieldMetadata{
		APIName:    "ChildCount__c",
		Type:       constants.FieldTypeRollupSummary,
		Createable: false,
		Updateable: false,
		RollupConfig: &models.RollupConfig{
			SummaryObject:     "Child__c",
			RelationshipField: "ParentId__c",
			CalcType:          "COUNT",
			MaintenanceMode:   "eventual",
		},
	})
	makeFieldsMap(parent)
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
		},
		childExists: true,
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	err := ps.PhysicalDeleteRaw(context.Background(), "Parent__c", "parent-1")
	if err == nil {
		t.Fatal("PhysicalDeleteRaw succeeded with a live child on an eventual COUNT rollup lookup")
	}
	apiErr, ok := err.(*apierr.Error)
	if !ok {
		t.Fatalf("error type = %T, want *apierr.Error: %v", err, err)
	}
	if apiErr.Status != http.StatusConflict {
		t.Fatalf("status = %d, want %d; err=%v", apiErr.Status, http.StatusConflict, err)
	}
	wantExists := "exists:Child__c:ParentId__c:parent-1"
	if got := relationshipDeleteCallIndex(repo.calls, wantExists); got < 0 {
		t.Fatalf("raw delete did not probe eventual COUNT rollup child edge %q: %#v", wantExists, repo.calls)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "physical:Parent__c:parent-1"); got >= 0 {
		t.Fatalf("raw delete physically deleted after blocked eventual COUNT child reference: %#v", repo.calls)
	}
}

func TestPhysicalDeleteRawIgnoresUncoveredChildSchemas(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	})
	child.ReadSource = &models.ObjectReadSourceConfig{Kind: "physical_table", PhysicalTable: "child_shadow"}
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
		},
		childExists: true,
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.PhysicalDeleteRaw(context.Background(), "Parent__c", "parent-1"); err != nil {
		t.Fatalf("PhysicalDeleteRaw with uncovered child schema: %v", err)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "exists:Child__c:ParentId__c:parent-1"); got >= 0 {
		t.Fatalf("uncovered child schema was probed: %#v", repo.calls)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "physical:Parent__c:parent-1"); got < 0 {
		t.Fatalf("raw delete did not physically delete uncovered case: %#v", repo.calls)
	}
}

func TestPhysicalDeleteRawIgnoresPlainLookupChildSchemas(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
		},
		childExists: true,
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.PhysicalDeleteRaw(context.Background(), "Parent__c", "parent-1"); err != nil {
		t.Fatalf("PhysicalDeleteRaw with plain lookup child schema: %v", err)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "exists:Child__c:ParentId__c:parent-1"); got >= 0 {
		t.Fatalf("plain lookup child schema was probed by raw-delete guard: %#v", repo.calls)
	}
	if got := relationshipDeleteCallIndex(repo.calls, "physical:Parent__c:parent-1"); got < 0 {
		t.Fatalf("raw delete did not physically delete plain lookup case: %#v", repo.calls)
	}
}

func relationshipDeleteCallIndex(calls []string, needle string) int {
	return relationshipDeleteCallIndexFrom(calls, needle, 0)
}

func relationshipDeleteCallIndexFrom(calls []string, needle string, start int) int {
	if start < 0 {
		start = 0
	}
	for i := start; i < len(calls); i++ {
		call := calls[i]
		if call == needle {
			return i
		}
	}
	return -1
}

func relationshipDeleteUpdateFor(calls []relationshipDeleteUpdate, tableName, id string) models.SObject {
	for _, call := range calls {
		if strings.EqualFold(call.table, tableName) && strings.EqualFold(call.id, id) {
			return call.updates
		}
	}
	return nil
}

func relationshipDeleteRecycleBinInsertFor(calls []relationshipDeleteUpdate, recordID string) models.SObject {
	for _, call := range calls {
		if !strings.EqualFold(call.table, constants.TableRecycleBin) {
			continue
		}
		if strings.EqualFold(stringValue(call.updates[constants.FieldRecordID]), recordID) {
			return call.updates
		}
	}
	return nil
}

type deleteAffectedParentRowsRollupProcessor struct {
	noopRollupProcessor
	keys []recordlocks.ParentKey
}

func (p deleteAffectedParentRowsRollupProcessor) AffectedParentRows(context.Context, string, models.SObject) ([]recordlocks.ParentKey, error) {
	return p.keys, nil
}

func TestDeletePrelocksRollupParentsBeforeTargetRow(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": {
				constants.FieldID:        "child-1",
				constants.FieldName:      "child",
				constants.FieldIsDeleted: false,
				"ParentId__c":            "parent-1",
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)
	ps.rollup = deleteAffectedParentRowsRollupProcessor{
		keys: []recordlocks.ParentKey{{ObjectName: "Parent__c", RecordID: "parent-1"}},
	}

	if err := ps.Delete(context.Background(), "Child__c", "child-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	parentLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
	childLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Child__c:child-1")
	if parentLockAt < 0 || childLockAt < 0 {
		t.Fatalf("missing lock calls: %#v", repo.calls)
	}
	if parentLockAt > childLockAt {
		t.Fatalf("rollup parent lock ran after child target lock: %#v", repo.calls)
	}
}

func TestDeletePrelocksLifecycleLookupParentBeforeTargetRow(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &cascade,
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": {
				constants.FieldID:        "child-1",
				constants.FieldName:      "child",
				constants.FieldIsDeleted: false,
				"ParentId__c":            "parent-1",
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Child__c", "child-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	parentLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
	childLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Child__c:child-1")
	if parentLockAt < 0 || childLockAt < 0 {
		t.Fatalf("missing lock calls: %#v", repo.calls)
	}
	if parentLockAt > childLockAt {
		t.Fatalf("lifecycle lookup parent lock ran after child target lock: %#v", repo.calls)
	}
}

func TestDeletePrelocksLifecycleLookupParentWithoutWritableReferenceRestriction(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:             "ParentId__c",
		Type:                constants.FieldTypeReference,
		ReferenceTo:         []string{"Parent__c"},
		WritableReferenceTo: []string{"DifferentParent__c"},
		DeleteRule:          &cascade,
		Createable:          true,
		Updateable:          true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": {
				constants.FieldID:        "child-1",
				constants.FieldName:      "child",
				constants.FieldIsDeleted: false,
				"ParentId__c":            "parent-1",
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	// WritableReferenceTo controls a user changing the FK. This delete only
	// reads the existing FK to establish parent-before-child locking, so it must
	// succeed even though a normal write of Parent__c through this field is not
	// permitted for the current user context.
	if err := ps.Delete(context.Background(), "Child__c", "child-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete with restricted WritableReferenceTo: %v", err)
	}
	parentLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
	childLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Child__c:child-1")
	if parentLockAt < 0 || childLockAt < 0 || parentLockAt > childLockAt {
		t.Fatalf("restricted reference lost lifecycle parent lock ordering: %#v", repo.calls)
	}
}

func TestDeletePrelocksSetNullChildParentsBeforeTargetRow(t *testing.T) {
	setNull := constants.DeleteRuleSetNull
	parent := relationshipDeleteParentSchema()
	account := relationshipDeleteParentSchema()
	account.APIName = "Account__c"
	account.Label = "Account"
	account.PluralLabel = "Accounts"
	account.Fields = append(account.Fields, deleteSetNullRollupField("Child__c", "AccountId__c"))
	makeFieldsMap(account)

	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	child.Fields = append(child.Fields, models.FieldMetadata{
		APIName:     "AccountId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account__c"},
		Createable:  true,
		Updateable:  true,
	})
	makeFieldsMap(child)

	childRow := models.SObject{
		constants.FieldID:        "child-1",
		constants.FieldName:      "child",
		constants.FieldIsDeleted: false,
		"ParentId__c":            "parent-1",
		"AccountId__c":           "account-1",
	}
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Account__c": {
				constants.FieldID:        "account-1",
				constants.FieldName:      "account",
				constants.FieldIsDeleted: false,
			},
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": childRow,
		},
		children: map[string][]models.SObject{
			"Child__c|ParentId__c|parent-1": {{constants.FieldID: "child-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, account, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	accountLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Account__c:account-1")
	parentLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
	if accountLockAt < 0 || parentLockAt < 0 {
		t.Fatalf("missing lock calls: %#v", repo.calls)
	}
	if accountLockAt > parentLockAt {
		t.Fatalf("set-null child secondary parent lock ran after deleted parent lock: %#v", repo.calls)
	}
}

func TestDeleteRetriesWhenSetNullChildAppearsAfterInitialLockPlan(t *testing.T) {
	setNull := constants.DeleteRuleSetNull
	parent := relationshipDeleteParentSchema()
	account := relationshipDeleteParentSchema()
	account.APIName = "Account__c"
	account.Label = "Account"
	account.PluralLabel = "Accounts"
	account.Fields = append(account.Fields, deleteSetNullRollupField("Child__c", "AccountId__c"))
	makeFieldsMap(account)

	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	child.Fields = append(child.Fields, models.FieldMetadata{
		APIName:     "AccountId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account__c"},
		Createable:  true,
		Updateable:  true,
	})
	makeFieldsMap(child)

	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Account__c": {
				constants.FieldID:        "account-1",
				constants.FieldName:      "account",
				constants.FieldIsDeleted: false,
			},
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": {
				constants.FieldID:        "child-1",
				constants.FieldName:      "child",
				constants.FieldIsDeleted: false,
				"ParentId__c":            "parent-1",
				"AccountId__c":           "account-1",
			},
		},
		childrenFn: func(childTable string, foreignKey string, parentID string, call int) ([]models.SObject, error) {
			if childTable != "Child__c" || foreignKey != "ParentId__c" || parentID != "parent-1" {
				return nil, nil
			}
			if call == 1 {
				return nil, nil
			}
			return []models.SObject{{constants.FieldID: "child-1"}}, nil
		},
	}
	ps := newRollupBoundaryTestService(repo, account, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	firstParentLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Parent__c:parent-1")
	accountLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Account__c:account-1")
	if firstParentLockAt < 0 || accountLockAt < 0 {
		t.Fatalf("missing lock calls: %#v", repo.calls)
	}
	if accountLockAt < firstParentLockAt {
		t.Fatalf("test did not exercise post-lock retry path; calls=%#v", repo.calls)
	}
	retriedParentLockAt := relationshipDeleteCallIndexFrom(repo.calls, "lock:Parent__c:parent-1", firstParentLockAt+1)
	if retriedParentLockAt < 0 {
		t.Fatalf("missing retried parent lock: %#v", repo.calls)
	}
	if accountLockAt > retriedParentLockAt {
		t.Fatalf("retry did not force secondary parent before retried parent lock: %#v", repo.calls)
	}
}

func TestDeletePrelocksCascadeReachableSetNullDescendantParents(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	setNull := constants.DeleteRuleSetNull

	account := relationshipDeleteParentSchema()
	account.APIName = "Account__c"
	account.Label = "Account"
	account.PluralLabel = "Accounts"
	account.Fields = append(account.Fields, deleteSetNullRollupField("Leaf__c", "AccountId__c"))
	makeFieldsMap(account)

	root := relationshipDeleteParentSchema()
	root.APIName = "Root__c"
	root.Label = "Root"
	root.PluralLabel = "Roots"
	makeFieldsMap(root)

	bridge := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "RootId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Root__c"},
		DeleteRule:  &cascade,
		Createable:  true,
		Updateable:  true,
	})
	bridge.APIName = "Bridge__c"
	bridge.Label = "Bridge"
	bridge.PluralLabel = "Bridges"
	makeFieldsMap(bridge)

	leaf := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "BridgeId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Bridge__c"},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	leaf.APIName = "Leaf__c"
	leaf.Label = "Leaf"
	leaf.PluralLabel = "Leaves"
	leaf.Fields = append(leaf.Fields, models.FieldMetadata{
		APIName:     "AccountId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account__c"},
		Createable:  true,
		Updateable:  true,
	})
	makeFieldsMap(leaf)

	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Account__c": {constants.FieldID: "account-1", constants.FieldName: "account", constants.FieldIsDeleted: false},
			"Root__c":    {constants.FieldID: "root-1", constants.FieldName: "root", constants.FieldIsDeleted: false},
			"Bridge__c":  {constants.FieldID: "bridge-1", constants.FieldName: "bridge", constants.FieldIsDeleted: false, "RootId__c": "root-1"},
			"Leaf__c": {
				constants.FieldID:        "leaf-1",
				constants.FieldName:      "leaf",
				constants.FieldIsDeleted: false,
				"BridgeId__c":            "bridge-1",
				"AccountId__c":           "account-1",
			},
		},
		children: map[string][]models.SObject{
			"Bridge__c|RootId__c|root-1":   {{constants.FieldID: "bridge-1"}},
			"Leaf__c|BridgeId__c|bridge-1": {{constants.FieldID: "leaf-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, account, root, bridge, leaf)

	if err := ps.Delete(context.Background(), "Root__c", "root-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete: %v", err)
	}

	accountLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Account__c:account-1")
	rootLockAt := relationshipDeleteCallIndex(repo.calls, "lock:Root__c:root-1")
	if accountLockAt < 0 || rootLockAt < 0 {
		t.Fatalf("missing lock calls: %#v", repo.calls)
	}
	if accountLockAt > rootLockAt {
		t.Fatalf("cascade-reachable set-null secondary parent lock ran after root lock: %#v", repo.calls)
	}
}

func TestExclusiveSiblingDeleteDirectAnnotatedObjectSoftDeletes(t *testing.T) {
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"LineItem__c": {
				constants.FieldID:        "line-1",
				"ParentId":               "parent-1",
				"IsPrimary__c":           true,
				constants.FieldIsDeleted: false,
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, exclusiveSiblingParentSchema(), exclusiveSiblingChildSchema())

	if err := ps.Delete(context.Background(), "LineItem__c", "line-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete annotated object: %v", err)
	}
	if len(repo.updateCalls) != 1 {
		t.Fatalf("annotated delete update calls = %#v, want one soft delete", repo.updateCalls)
	}
	if got := repo.updateCalls[0].updates[constants.FieldIsDeleted]; got != 1 {
		t.Fatalf("annotated delete IsDeleted update = %#v, want 1", got)
	}
	if len(repo.insertCalls) != 1 || repo.insertCalls[0].table != constants.TableRecycleBin {
		t.Fatalf("annotated delete recycle-bin inserts = %#v, want one tombstone", repo.insertCalls)
	}
	if got := repo.insertCalls[0].updates[constants.FieldSysRecycleBin_DeleteRootRecordID]; got != "line-1" {
		t.Fatalf("tombstone DeleteRootRecordId = %#v, want line-1", got)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("annotated soft delete physically deleted records: %#v", repo.physicalCalls)
	}
}

func TestExclusiveSiblingDeleteCascadeToAnnotatedChildSoftDeletesChild(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteExclusiveChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &cascade,
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": {
				constants.FieldID:        "child-1",
				constants.FieldName:      "child",
				constants.FieldIsDeleted: false,
				"ParentId__c":            "parent-1",
				"IsPrimary__c":           true,
			},
		},
		children: map[string][]models.SObject{
			"Child__c|ParentId__c|parent-1": {{constants.FieldID: "child-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete cascading to annotated child: %v", err)
	}
	if got := relationshipDeleteUpdateFor(repo.updateCalls, "Child__c", "child-1"); got == nil || got[constants.FieldIsDeleted] != 1 {
		t.Fatalf("cascade child soft-delete update missing: %#v", repo.updateCalls)
	}
	if got := relationshipDeleteUpdateFor(repo.updateCalls, "Parent__c", "parent-1"); got == nil || got[constants.FieldIsDeleted] != 1 {
		t.Fatalf("cascade parent soft-delete update missing: %#v", repo.updateCalls)
	}
	childTombstone := relationshipDeleteRecycleBinInsertFor(repo.insertCalls, "child-1")
	if childTombstone == nil {
		t.Fatalf("cascade child tombstone missing: %#v", repo.insertCalls)
	}
	if got := childTombstone[constants.FieldSysRecycleBin_CascadeParentRecordID]; got != "parent-1" {
		t.Fatalf("child tombstone CascadeParentRecordId = %#v, want parent-1", got)
	}
	if got := childTombstone[constants.FieldSysRecycleBin_CascadeFieldAPIName]; got != "ParentId__c" {
		t.Fatalf("child tombstone CascadeFieldApiName = %#v, want ParentId__c", got)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("cascade soft delete physically deleted records: %#v", repo.physicalCalls)
	}
}

func TestExclusiveSiblingDeleteSetNullAnnotatedChildUsesUpdateLifecycle(t *testing.T) {
	setNull := constants.DeleteRuleSetNull
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteExclusiveChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {
				constants.FieldID:        "parent-1",
				constants.FieldName:      "parent",
				constants.FieldIsDeleted: false,
			},
			"Child__c": {
				constants.FieldID:        "child-1",
				constants.FieldName:      "child",
				constants.FieldIsDeleted: false,
				"ParentId__c":            "parent-1",
				"IsPrimary__c":           true,
			},
		},
		children: map[string][]models.SObject{
			"Child__c|ParentId__c|parent-1": {{constants.FieldID: "child-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete with SetNull annotated child: %v", err)
	}
	childUpdate := relationshipDeleteUpdateFor(repo.updateCalls, "Child__c", "child-1")
	if childUpdate == nil {
		t.Fatalf("set-null child update missing: %#v", repo.updateCalls)
	}
	if val, ok := childUpdate["ParentId__c"]; !ok || val != nil {
		t.Fatalf("set-null child ParentId__c update = %#v (present=%v), want nil", val, ok)
	}
	parentUpdate := relationshipDeleteUpdateFor(repo.updateCalls, "Parent__c", "parent-1")
	if parentUpdate == nil || parentUpdate[constants.FieldIsDeleted] != 1 {
		t.Fatalf("set-null parent soft-delete update missing: %#v", repo.updateCalls)
	}
	if len(repo.physicalCalls) != 0 {
		t.Fatalf("set-null soft delete physically deleted records: %#v", repo.physicalCalls)
	}
}

func TestExclusiveSiblingDeleteRecordsAllowsAnnotatedTargetsPartialSuccess(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:        "probe-1",
			constants.FieldIsDeleted: 0,
		},
		recordTypes: map[string]string{
			"probe-1": "ProbeObject",
			"line-1":  "LineItem__c",
		},
	}
	ps := newRollupBoundaryTestService(repo, recordDeleteEntrypointSchema(), exclusiveSiblingParentSchema(), exclusiveSiblingChildSchema())

	results, err := ps.DeleteRecords(context.Background(), []string{"probe-1", "line-1"}, false, recordEntrypointUser())
	if err != nil {
		t.Fatalf("DeleteRecords(allOrNone=false): %v", err)
	}
	if len(results) != 2 || !results[0].Success || !results[1].Success {
		t.Fatalf("DeleteRecords(allOrNone=false) results = %#v, want two successes", results)
	}
	if repo.updated == nil || repo.updated[constants.FieldIsDeleted] != 1 {
		t.Fatalf("DeleteRecords(allOrNone=false) did not soft-delete rows: %#v", repo.updated)
	}
	if repo.inserted == nil || repo.inserted[constants.FieldSysRecycleBin_DeleteOperationID] == "" {
		t.Fatalf("DeleteRecords(allOrNone=false) tombstone missing lineage: %#v", repo.inserted)
	}
}

func TestExclusiveSiblingDeleteRecordsAllOrNoneFailsClosedForAnnotatedTargets(t *testing.T) {
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:        "line-1",
			constants.FieldIsDeleted: 0,
		},
		recordTypes: map[string]string{
			"line-1": "LineItem__c",
		},
	}
	ps := newRollupBoundaryTestService(repo, recordDeleteEntrypointSchema(), exclusiveSiblingParentSchema(), exclusiveSiblingChildSchema())

	results, err := ps.DeleteRecords(context.Background(), []string{"line-1"}, true, recordEntrypointUser())
	if err == nil {
		t.Fatalf("DeleteRecords(allOrNone=true) returned results %#v, want fail-closed validation error", results)
	}
	if !appErrors.IsValidation(err) {
		t.Fatalf("DeleteRecords(allOrNone=true) error = %T %v, want validation error", err, err)
	}
	if repo.updated != nil || repo.inserted != nil {
		t.Fatalf("DeleteRecords(allOrNone=true) mutated before fail-closed guard: updated=%#v inserted=%#v", repo.updated, repo.inserted)
	}
}

func TestExclusiveSiblingDeleteRecordsPartialSuccessWhenLaterTargetUnresolved(t *testing.T) {
	lineLookups := 0
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:        "probe-1",
			constants.FieldIsDeleted: 0,
		},
		recordTypeFn: func(_ context.Context, id string) (string, error) {
			switch id {
			case "probe-1":
				return "ProbeObject", nil
			case "line-1":
				lineLookups++
				if lineLookups == 1 {
					return "", nil
				}
				return "LineItem__c", nil
			default:
				return "", nil
			}
		},
	}
	ps := newRollupBoundaryTestService(repo, recordDeleteEntrypointSchema(), exclusiveSiblingParentSchema(), exclusiveSiblingChildSchema())

	results, err := ps.DeleteRecords(context.Background(), []string{"probe-1", "line-1"}, false, recordEntrypointUser())
	if err != nil {
		t.Fatalf("DeleteRecords(allOrNone=false): %v", err)
	}
	if lineLookups != 1 {
		t.Fatalf("line-1 GetRecordType calls = %d, want single preflight lookup", lineLookups)
	}
	if len(results) != 2 || !results[0].Success || results[1].Success {
		t.Fatalf("DeleteRecords(allOrNone=false) results = %#v, want first success and second failure", results)
	}
	if repo.updated == nil || repo.updated[constants.FieldIsDeleted] != 1 {
		t.Fatalf("DeleteRecords(allOrNone=false) did not soft-delete first target: %#v", repo.updated)
	}
}

func TestDeleteBlockedByAmbiguousEffectiveRelationship(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records:     map[string]models.SObject{"Parent__c": {constants.FieldID: "parent-1"}},
		childExists: true,
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser())
	if err == nil {
		t.Fatal("Delete succeeded with live child rows under ambiguous relationship")
	}
	apiErr, ok := err.(*apierr.Error)
	if !ok {
		t.Fatalf("error type = %T, want *apierr.Error", err)
	}
	if apiErr.Status != http.StatusConflict {
		t.Fatalf("status = %d, want %d; err=%v", apiErr.Status, http.StatusConflict, err)
	}
	if len(repo.updateCalls) != 0 {
		t.Fatalf("blocked delete updated records: %#v", repo.updateCalls)
	}
}

func TestDeleteSucceedsForAmbiguousEffectiveRelationshipWithoutChildren(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{"Parent__c": {constants.FieldID: "parent-1"}},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete without children: %v", err)
	}
	if len(repo.updateCalls) != 1 {
		t.Fatalf("update calls = %#v, want one parent soft delete", repo.updateCalls)
	}
	if got := repo.updateCalls[0].updates[constants.FieldIsDeleted]; got != 1 {
		t.Fatalf("IsDeleted update = %v, want 1", got)
	}
}

func TestDeleteClearsSetNullRelationshipChildren(t *testing.T) {
	setNull := constants.DeleteRuleSetNull
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "ParentId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Parent__c"},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {constants.FieldID: "parent-1"},
			"Child__c":  {constants.FieldID: "child-1", "ParentId__c": "parent-1"},
		},
		childExists: true,
		children: map[string][]models.SObject{
			"Child__c|ParentId__c|parent-1": {{constants.FieldID: "child-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)
	eventBus := &relationshipDeleteEventBus{}
	ps.eventBus = eventBus

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete with set-null child: %v", err)
	}

	var sawSetNull bool
	var sawParentDelete bool
	for _, call := range repo.updateCalls {
		if call.table == "Child__c" && call.id == "child-1" {
			if v, ok := call.updates["ParentId__c"]; ok && v == nil {
				sawSetNull = true
			}
		}
		if call.table == "Parent__c" && call.id == "parent-1" && call.updates[constants.FieldIsDeleted] == 1 {
			sawParentDelete = true
		}
	}
	if !sawSetNull {
		t.Fatalf("set-null child relationship was not cleared; update calls=%#v", repo.updateCalls)
	}
	if !sawParentDelete {
		t.Fatalf("parent was not soft-deleted; update calls=%#v", repo.updateCalls)
	}
	updateEvent, ok := eventBus.saw("Child__c", sharedEvents.RecordBeforeUpdate)
	if !ok {
		t.Fatalf("set-null child clear did not run through the update lifecycle; events=%#v", eventBus.events)
	}
	if updateEvent.securityContext != constants.SystemContext {
		t.Fatalf("set-null child update security context = %v, want SystemContext", updateEvent.securityContext)
	}
}

func TestDeleteClearsSetNullPolymorphicDiscriminator(t *testing.T) {
	setNull := constants.DeleteRuleSetNull
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:       "ParentId__c",
		Type:          constants.FieldTypeReference,
		ReferenceTo:   []string{"Parent__c", "Other__c"},
		IsPolymorphic: true,
		DeleteRule:    &setNull,
		Createable:    true,
		Updateable:    true,
	})
	child.Fields = append(child.Fields, models.FieldMetadata{
		APIName: "ParentId__c_type",
		Type:    constants.FieldTypeString,
	})
	makeFieldsMap(child)
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {constants.FieldID: "parent-1"},
			"Child__c":  {constants.FieldID: "child-1", "ParentId__c": "parent-1", "ParentId__c_type": "Parent__c"},
		},
		childExists: true,
		children: map[string][]models.SObject{
			"Child__c|ParentId__c|parent-1": {{constants.FieldID: "child-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete with polymorphic set-null child: %v", err)
	}

	for _, call := range repo.updateCalls {
		if call.table != "Child__c" || call.id != "child-1" {
			continue
		}
		if v, ok := call.updates["ParentId__c"]; !ok || v != nil {
			t.Fatalf("ParentId__c update = %#v (present=%v), want nil", v, ok)
		}
		if v, ok := call.updates["ParentId__c_type"]; !ok || v != nil {
			t.Fatalf("ParentId__c_type update = %#v (present=%v), want nil", v, ok)
		}
		return
	}
	t.Fatalf("no child set-null update found; update calls=%#v", repo.updateCalls)
}

func TestDeleteTransactionObjectNamesIncludesSetNullChildSourceObject(t *testing.T) {
	setNull := constants.DeleteRuleSetNull
	parent := relationshipDeleteParentSchema()
	parent.APIName = constants.TableUserRole
	parent.Label = "User Role"
	parent.PluralLabel = "User Roles"
	makeFieldsMap(parent)

	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     constants.FieldUserRoleID,
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{constants.TableUserRole},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	child.APIName = constants.TableUser
	child.Label = "User"
	child.PluralLabel = "Users"
	makeFieldsMap(child)

	ps := newRollupBoundaryTestService(&relationshipDeleteRepo{}, parent, child)
	names := ps.deleteTransactionObjectNames(context.Background(), constants.TableUserRole)
	for _, name := range names {
		if name == constants.TableUser {
			return
		}
	}
	t.Fatalf("delete transaction object names = %v, want SetNull child source object %q", names, constants.TableUser)
}

func TestDeleteRejectsSecurityParentWithLiveInboundReferenceBeforeSoftDelete(t *testing.T) {
	schema := securitySoftDeleteReferenceGuardDeleteSchema(
		constants.TableProfile,
		[]models.RecordWriteValidationPolicy{
			{Phase: string(contract.RecordWriteValidationBeforeDelete), Capability: securityWriteValidateSoftDeleteReferenceGuardCapability},
		},
		nil,
	)
	repo := &captureRecordRepo{
		old: models.SObject{
			constants.FieldID:               "00e-profile",
			constants.FieldProfile_IsSystem: false,
			constants.FieldIsDeleted:        false,
		},
		getChildrenFn: func(_ context.Context, childTable string, foreignKey string, parentID string) ([]models.SObject, error) {
			if parentID != "00e-profile" {
				t.Fatalf("GetChildren parentID = %q, want 00e-profile", parentID)
			}
			if childTable == constants.TableUser && foreignKey == constants.FieldUser_ProfileID {
				return []models.SObject{{
					constants.FieldID:        "005-user",
					constants.FieldIsDeleted: false,
				}}, nil
			}
			return nil, nil
		},
	}
	ps := newRecordEntrypointTestService(schema, repo)

	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "security parent soft-delete guard regression")
	err := ps.Delete(ctx, constants.TableProfile, "00e-profile", recordEntrypointUser())
	if err == nil {
		t.Fatalf("Delete succeeded for Profile with live User.ProfileId reference")
	}
	want := softDeleteReferenceGuardConflictMessage(constants.TableProfile, softDeleteReferenceGuardEdge{
		childObjectName: constants.TableUser,
		fieldName:       constants.FieldUser_ProfileID,
	})
	if !strings.Contains(err.Error(), want) {
		t.Fatalf("Delete error = %v, want %q", err, want)
	}
	if repo.updated != nil {
		t.Fatalf("Profile was soft-deleted despite live child guard: %#v", repo.updated)
	}
}

func securitySoftDeleteReferenceGuardDeleteSchema(objectName string, validators []models.RecordWriteValidationPolicy, ownerResolution *models.OwnerResolutionSpec) *models.ObjectMetadata {
	schema := writeValidationProbeSchemaForPhase(contract.RecordWriteValidationBeforeDelete)
	schema.APIName = objectName
	schema.Label = objectName
	schema.PluralLabel = objectName + "s"
	schema.AccessPolicy = &models.ObjectAccessPolicy{
		WriteValidation: &models.RecordWriteValidationAccessPolicy{
			Validators:      append([]models.RecordWriteValidationPolicy(nil), validators...),
			OwnerResolution: ownerResolution,
		},
	}
	makeFieldsMap(schema)
	return schema
}

func TestDeleteTransactionObjectNamesIncludesCascadeReachableSetNullDescendant(t *testing.T) {
	cascade := constants.DeleteRuleCascade
	setNull := constants.DeleteRuleSetNull

	root := relationshipDeleteParentSchema()
	root.APIName = "Root__c"
	root.Label = "Root"
	root.PluralLabel = "Roots"
	makeFieldsMap(root)

	bridge := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "RootId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Root__c"},
		DeleteRule:  &cascade,
		Createable:  true,
		Updateable:  true,
	})
	bridge.APIName = "Bridge__c"
	bridge.Label = "Bridge"
	bridge.PluralLabel = "Bridges"
	makeFieldsMap(bridge)

	user := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     "BridgeId__c",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Bridge__c"},
		DeleteRule:  &setNull,
		Createable:  true,
		Updateable:  true,
	})
	user.APIName = constants.TableUser
	user.Label = "User"
	user.PluralLabel = "Users"
	makeFieldsMap(user)

	ps := newRollupBoundaryTestService(&relationshipDeleteRepo{}, root, bridge, user)
	names := ps.deleteTransactionObjectNames(context.Background(), "Root__c")

	got := map[string]bool{}
	for _, name := range names {
		got[name] = true
	}
	for _, want := range []string{"Root__c", "Bridge__c", constants.TableUser} {
		if !got[want] {
			t.Fatalf("delete transaction object names = %v, want %q", names, want)
		}
	}
}

func TestDeleteCascadesMasterDetailRelationshipField(t *testing.T) {
	parent := relationshipDeleteParentSchema()
	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:        "ParentId__c",
		Type:           constants.FieldTypeMasterDetail,
		IsMasterDetail: true,
		ReferenceTo:    []string{"Parent__c"},
		Createable:     true,
		Updateable:     true,
	})
	repo := &relationshipDeleteRepo{
		records: map[string]models.SObject{
			"Parent__c": {constants.FieldID: "parent-1"},
			"Child__c":  {constants.FieldID: "child-1"},
		},
		childExists: true,
		children: map[string][]models.SObject{
			"Child__c|ParentId__c|parent-1": {{constants.FieldID: "child-1"}},
		},
	}
	ps := newRollupBoundaryTestService(repo, parent, child)

	if err := ps.Delete(context.Background(), "Parent__c", "parent-1", recordEntrypointUser()); err != nil {
		t.Fatalf("Delete with master-detail child: %v", err)
	}

	var sawChildDelete bool
	for _, call := range repo.updateCalls {
		if call.table == "Child__c" && call.id == "child-1" && call.updates[constants.FieldIsDeleted] == 1 {
			sawChildDelete = true
		}
	}
	if !sawChildDelete {
		t.Fatalf("master-detail child was not soft-deleted; update calls=%#v", repo.updateCalls)
	}
}

func TestAuthorizeLockedDeleteRejectsGenericFieldMetadataLifecycleInEverySecurityContext(t *testing.T) {
	ps := &PersistenceService{}
	schema := &models.ObjectMetadata{APIName: constants.TableField}
	contexts := map[string]context.Context{
		"user":   context.Background(),
		"system": constants.WithSecurityContext(context.Background(), constants.SystemContext, "field lifecycle guard test"),
	}
	for name, ctx := range contexts {
		t.Run(name, func(t *testing.T) {
			err := ps.authorizeLockedDelete(
				ctx,
				schema,
				constants.TableField,
				models.SObject{constants.FieldID: "field-1"},
				nil,
				func() error { return fmt.Errorf("unexpected object authorization") },
				func() error { return fmt.Errorf("unexpected record authorization") },
			)
			if err == nil || !strings.Contains(err.Error(), "field lifecycle is owned by schema persistence") {
				t.Fatalf("authorizeLockedDelete() error = %v, want unbypassable field lifecycle rejection", err)
			}
		})
	}
}
