package records

import (
	"context"
	"fmt"
	"strings"
	"testing"

	eventFeature "github.com/nexuscrm/events"
	"github.com/nexuscrm/kernel/pkg/formula"
	"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"
	"github.com/nexuscrm/shared/pkg/models"
)

type rollupSummaryRecordRepo struct {
	stubUpsertRecordRepo
	old              models.SObject
	updated          models.SObject
	bulkInserted     []models.SObject
	bulkColumns      []string
	findOneByKey     map[string]models.SObject
	findOneCalls     int
	findManyCalls    int
	updateCalled     bool
	bulkInsertCalled bool
}

func (r *rollupSummaryRecordRepo) storeRecord(tableName string, record models.SObject) {
	id, _ := record[constants.FieldID].(string)
	if id == "" {
		return
	}
	if r.findOneByKey == nil {
		r.findOneByKey = make(map[string]models.SObject)
	}
	r.findOneByKey[tableName+"/"+id] = cloneSObject(record)
}

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

func (r *rollupSummaryRecordRepo) Insert(ctx context.Context, t string, rec models.SObject) error {
	if r.insertFn != nil {
		if err := r.insertFn(ctx, t, rec); err != nil {
			return err
		}
	}
	r.storeRecord(t, rec)
	return nil
}

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

func (r *rollupSummaryRecordRepo) FindOne(ctx context.Context, tableName string, id string) (models.SObject, error) {
	r.findOneCalls++
	if r.findOneByKey != nil {
		return cloneSObject(r.findOneByKey[tableName+"/"+id]), nil
	}
	if oldID, _ := r.old[constants.FieldID].(string); oldID == id {
		return cloneSObject(r.old), nil
	}
	return nil, nil
}

func (r *rollupSummaryRecordRepo) FindMany(ctx context.Context, tableName string, ids []string) (map[string]models.SObject, error) {
	r.findManyCalls++
	out := make(map[string]models.SObject, len(ids))
	for _, id := range ids {
		if r.findOneByKey == nil {
			continue
		}
		if row := r.findOneByKey[tableName+"/"+id]; row != nil {
			out[id] = cloneSObject(row)
		}
	}
	return out, nil
}

func (r *rollupSummaryRecordRepo) BulkInsert(ctx context.Context, t string, recs []models.SObject, cols []string, opts interface{}) error {
	r.bulkInsertCalled = true
	r.bulkColumns = append([]string(nil), cols...)
	r.bulkInserted = cloneSObjects(recs)
	for _, rec := range recs {
		r.storeRecord(t, rec)
	}
	return nil
}

func (r *rollupSummaryRecordRepo) BulkInsertWithLanding(ctx context.Context, t string, recs []models.SObject, cols []string, opts interface{}) ([]bool, error) {
	r.bulkInsertCalled = true
	r.bulkColumns = append([]string(nil), cols...)
	r.bulkInserted = cloneSObjects(recs)
	for _, rec := range recs {
		r.storeRecord(t, rec)
	}
	landed := make([]bool, len(recs))
	for i := range landed {
		landed[i] = true
	}
	return landed, nil
}

func newRollupSummaryTestService(schema *models.ObjectMetadata, repo *rollupSummaryRecordRepo) *PersistenceService {
	metaRepo := &stubUpsertMetaRepo{schema: schema}
	metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	return &PersistenceService{
		repo:                repo,
		rollup:              noopRollupProcessor{},
		metadata:            metaSvc,
		permissions:         permSvc,
		eventBus:            &eventFeature.NoOpEventBus{},
		validator:           metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator:     validation.NewSchemaValidator(),
		txManager:           inlineTxManager{},
		parentLaneAdmission: &fakeParentLaneAdmissionController{},
	}
}

type finalImageRollupProcessor struct {
	noopRollupProcessor
	mutateRecord func(objectName string, record models.SObject)
}

func (p *finalImageRollupProcessor) ProcessInsertRollups(ctx context.Context, objectName string, record models.SObject) error {
	if p.mutateRecord != nil {
		p.mutateRecord(objectName, record)
	}
	return nil
}

func (p *finalImageRollupProcessor) ProcessBulkRollups(ctx context.Context, objectName string, records []models.SObject, landedRows []bool) error {
	if p.mutateRecord == nil {
		return nil
	}
	for i, record := range records {
		if i >= len(landedRows) || !landedRows[i] {
			continue
		}
		p.mutateRecord(objectName, record)
	}
	return nil
}

func rollupSummaryCreatedPayloadForRecord(t *testing.T, payloads []interface{}, id string) eventFeature.RecordEventPayload {
	t.Helper()
	for _, payload := range payloads {
		envelope, ok := payload.(eventFeature.InternalRecordEventEnvelope)
		if !ok {
			t.Fatalf("outbox payload type = %T, want InternalRecordEventEnvelope", payload)
		}
		if envelope.PublicPayload.Record[constants.FieldID] == id {
			return envelope.PublicPayload
		}
	}
	t.Fatalf("missing RecordCreated payload for %s; payloads=%#v", id, payloads)
	return eventFeature.RecordEventPayload{}
}

func jiraLikeRollupSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Epic__c",
		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:    "StoryPointTotal__c",
				Type:       constants.FieldTypeRollupSummary,
				Createable: false,
				Updateable: false,
			},
		},
	}
	makeFieldsMap(schema)
	return schema
}

func jiraLikeRequiredPicklistRollupSchema() *models.ObjectMetadata {
	schema := jiraLikeRollupSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName:    "Channel__c",
		Type:       constants.FieldTypePicklist,
		Createable: true,
		Updateable: true,
		Required:   true,
		Options:    models.PicklistOptionsFromStrings([]string{"Direct", "Partner"}),
	})
	makeFieldsMap(schema)
	return schema
}

func jiraLikeCountRollupSchema() *models.ObjectMetadata {
	schema := jiraLikeRollupSchema()
	for i := range schema.Fields {
		if schema.Fields[i].APIName == "StoryPointTotal__c" {
			schema.Fields[i].RollupConfig = &models.RollupConfig{
				CalcType:          "COUNT",
				SummaryObject:     "Story__c",
				SummaryField:      "Id",
				RelationshipField: "Epic__c",
			}
		}
	}
	makeFieldsMap(schema)
	return schema
}

func withAfterCreateLifecycleCapability(schema *models.ObjectMetadata) *models.ObjectMetadata {
	if schema == nil {
		return schema
	}
	schema.AccessPolicy = &models.ObjectAccessPolicy{
		Lifecycle: &models.RecordLifecycleAccessPolicy{
			Hooks: []models.RecordLifecycleHookPolicy{{
				Phase:      string(recordLifecycleAfterCreateInTx),
				Capability: "test.final-create-image",
			}},
		},
	}
	return schema
}

func registerFinalCreateImageLifecycleHook(t *testing.T, ps *PersistenceService) {
	t.Helper()
	if err := ps.RegisterRecordLifecycleHook(contract.RecordLifecycleRegistration{
		Capability: "test.final-create-image",
		Phase:      recordLifecycleAfterCreateInTx,
		Hook: func(context.Context, contract.RecordLifecycleEvent) error {
			return nil
		},
	}); err != nil {
		t.Fatalf("RegisterRecordLifecycleHook: %v", err)
	}
}

func TestInsertInitializesCountRollupSummaryToZero(t *testing.T) {
	var inserted models.SObject
	repo := &rollupSummaryRecordRepo{}
	repo.insertFn = func(ctx context.Context, t string, rec models.SObject) error {
		inserted = cloneSObject(rec)
		return nil
	}
	ps := newRollupSummaryTestService(jiraLikeCountRollupSchema(), repo)

	_, err := ps.Insert(context.Background(), "Epic__c", models.SObject{
		constants.FieldName: "Sprint 1",
	}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Insert: %v", err)
	}
	if got, ok := inserted["StoryPointTotal__c"]; !ok || got != int64(0) {
		t.Fatalf("inserted record = %#v, want StoryPointTotal__c initialized to 0", inserted)
	}
}

func TestInsertReturnsAndEmitsFinalCreateImageAfterDerivedPostInsertWrite(t *testing.T) {
	var inserted models.SObject
	repo := &rollupSummaryRecordRepo{}
	repo.insertFn = func(ctx context.Context, t string, rec models.SObject) error {
		inserted = cloneSObject(rec)
		return nil
	}
	ps := newRollupSummaryTestService(withAfterCreateLifecycleCapability(jiraLikeCountRollupSchema()), repo)
	registerFinalCreateImageLifecycleHook(t, ps)
	ps.rollup = &finalImageRollupProcessor{mutateRecord: func(objectName string, record models.SObject) {
		id, _ := record[constants.FieldID].(string)
		repo.findOneByKey[objectName+"/"+id]["StoryPointTotal__c"] = int64(1)
	}}
	outboxRepo := &exclusiveSiblingOutboxRepo{}
	ps.outbox = eventFeature.NewOutboxService(outboxRepo, &eventFeature.NoOpEventBus{}, inlineTxManager{})

	created, err := ps.Insert(context.Background(), "Epic__c", models.SObject{
		constants.FieldName: "Sprint 1",
	}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Insert: %v", err)
	}
	if got := inserted["StoryPointTotal__c"]; got != int64(0) {
		t.Fatalf("initial insert image StoryPointTotal__c = %v, want 0 before derived write", got)
	}
	if got := created["StoryPointTotal__c"]; got != int64(1) {
		t.Fatalf("returned create image StoryPointTotal__c = %v, want final DB value 1", got)
	}
	id, _ := created[constants.FieldID].(string)
	payload := rollupSummaryCreatedPayloadForRecord(t, outboxRepo.payloads, id)
	if got := payload.Record["StoryPointTotal__c"]; got != int64(1) {
		t.Fatalf("RecordCreated payload StoryPointTotal__c = %v, want final DB value 1", got)
	}
}

func TestInsertWithOutboxDoesNotReloadFinalImageWithoutPostCreateMutationRisk(t *testing.T) {
	repo := &rollupSummaryRecordRepo{}
	ps := newRollupSummaryTestService(jiraLikeCountRollupSchema(), repo)
	outboxRepo := &exclusiveSiblingOutboxRepo{}
	ps.outbox = eventFeature.NewOutboxService(outboxRepo, &eventFeature.NoOpEventBus{}, inlineTxManager{})

	created, err := ps.Insert(context.Background(), "Epic__c", models.SObject{
		constants.FieldName: "Sprint 1",
	}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Insert: %v", err)
	}
	if id, _ := created[constants.FieldID].(string); id == "" {
		t.Fatalf("created record missing Id: %#v", created)
	}
	if repo.findOneCalls != 0 {
		t.Fatalf("FindOne calls = %d, want 0 when only outbox is present", repo.findOneCalls)
	}
	if len(outboxRepo.payloads) != 1 {
		t.Fatalf("outbox payloads = %d, want 1", len(outboxRepo.payloads))
	}
}

func TestBulkInsertInitializesCountRollupSummaryToZero(t *testing.T) {
	repo := &rollupSummaryRecordRepo{}
	ps := newRollupSummaryTestService(jiraLikeCountRollupSchema(), repo)

	result, err := ps.BulkInsert(context.Background(), "Epic__c", []models.SObject{{
		constants.FieldName: "Sprint 1",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}
	if result.SuccessCount != 1 {
		t.Fatalf("BulkInsert success count = %d, want 1 (errors=%v)", result.SuccessCount, result.Errors)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulk inserted rows = %d, want 1", len(repo.bulkInserted))
	}
	if got, ok := repo.bulkInserted[0]["StoryPointTotal__c"]; !ok || got != int64(0) {
		t.Fatalf("bulk inserted record = %#v, want StoryPointTotal__c initialized to 0", repo.bulkInserted[0])
	}
}

func TestBulkInsertRecordCreatedPayloadUsesFinalCreateImageAfterDerivedPostInsertWrite(t *testing.T) {
	repo := &rollupSummaryRecordRepo{}
	ps := newRollupSummaryTestService(withAfterCreateLifecycleCapability(jiraLikeCountRollupSchema()), repo)
	registerFinalCreateImageLifecycleHook(t, ps)
	ps.rollup = &finalImageRollupProcessor{mutateRecord: func(objectName string, record models.SObject) {
		id, _ := record[constants.FieldID].(string)
		repo.findOneByKey[objectName+"/"+id]["StoryPointTotal__c"] = int64(1)
	}}
	outboxRepo := &exclusiveSiblingOutboxRepo{}
	ps.outbox = eventFeature.NewOutboxService(outboxRepo, &eventFeature.NoOpEventBus{}, inlineTxManager{})

	result, err := ps.BulkInsert(context.Background(), "Epic__c", []models.SObject{{
		constants.FieldName: "Sprint 1",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}
	if result.SuccessCount != 1 {
		t.Fatalf("BulkInsert success count = %d, want 1 (errors=%v)", result.SuccessCount, result.Errors)
	}
	id, _ := repo.bulkInserted[0][constants.FieldID].(string)
	if got := repo.bulkInserted[0]["StoryPointTotal__c"]; got != int64(0) {
		t.Fatalf("initial bulk insert image StoryPointTotal__c = %v, want 0 before derived write", got)
	}
	payload := rollupSummaryCreatedPayloadForRecord(t, outboxRepo.payloads, id)
	if got := payload.Record["StoryPointTotal__c"]; got != int64(1) {
		t.Fatalf("bulk RecordCreated payload StoryPointTotal__c = %v, want final DB value 1", got)
	}
}

func TestBulkInsertWithOutboxDoesNotReloadFinalImagesWithoutPostCreateMutationRisk(t *testing.T) {
	repo := &rollupSummaryRecordRepo{}
	ps := newRollupSummaryTestService(jiraLikeCountRollupSchema(), repo)
	outboxRepo := &exclusiveSiblingOutboxRepo{}
	ps.outbox = eventFeature.NewOutboxService(outboxRepo, &eventFeature.NoOpEventBus{}, inlineTxManager{})

	result, err := ps.BulkInsert(context.Background(), "Epic__c", []models.SObject{{
		constants.FieldName: "Sprint 1",
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}
	if result.SuccessCount != 1 {
		t.Fatalf("BulkInsert success count = %d, want 1 (errors=%v)", result.SuccessCount, result.Errors)
	}
	if repo.findManyCalls != 0 {
		t.Fatalf("FindMany calls = %d, want 0 when only outbox is present", repo.findManyCalls)
	}
	if len(outboxRepo.payloads) != 1 {
		t.Fatalf("outbox payloads = %d, want 1", len(outboxRepo.payloads))
	}
}

func TestUpdateDerivedPersistsStoredRollupSummary(t *testing.T) {
	repo := &rollupSummaryRecordRepo{
		old: models.SObject{
			constants.FieldID:   "epic-001",
			constants.FieldName: "Sprint 1",
		},
	}
	ps := newRollupSummaryTestService(jiraLikeRollupSchema(), repo)

	err := ps.UpdateDerived(context.Background(), "Epic__c", "epic-001", models.SObject{
		"StoryPointTotal__c": float64(8),
	}, recordEntrypointUser())
	if err != nil {
		t.Fatalf("UpdateDerived: %v", err)
	}
	if !repo.updateCalled {
		t.Fatal("expected UpdateDerived to reach repo.Update")
	}
	if got, ok := repo.updated["StoryPointTotal__c"]; !ok || got != float64(8) {
		t.Fatalf("repo.Update payload = %#v, want StoryPointTotal__c=8", repo.updated)
	}
}

func TestUpdatePreservesUntouchedHistoricalBlankRequiredPicklist(t *testing.T) {
	repo := &rollupSummaryRecordRepo{old: models.SObject{
		constants.FieldID:   "epic-001",
		constants.FieldName: "Sprint 1",
		"Channel__c":        "",
	}}
	ps := newRollupSummaryTestService(jiraLikeRequiredPicklistRollupSchema(), repo)

	if err := ps.Update(context.Background(), "Epic__c", "epic-001", models.SObject{
		constants.FieldName: "Sprint 2",
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("Update unrelated field with historical blank required picklist: %v", err)
	}
	if got := repo.updated[constants.FieldName]; got != "Sprint 2" {
		t.Fatalf("repo.Update Name = %#v, want Sprint 2", got)
	}
	if _, exists := repo.updated["Channel__c"]; exists {
		t.Fatalf("repo.Update payload = %#v, want untouched Channel__c omitted", repo.updated)
	}
}

func TestUpdateRejectsExplicitHistoricalBlankRequiredPicklist(t *testing.T) {
	for _, value := range []interface{}{"", nil} {
		t.Run(fmt.Sprintf("%#v", value), func(t *testing.T) {
			repo := &rollupSummaryRecordRepo{old: models.SObject{
				constants.FieldID:   "epic-001",
				constants.FieldName: "Sprint 1",
				"Channel__c":        "",
			}}
			ps := newRollupSummaryTestService(jiraLikeRequiredPicklistRollupSchema(), repo)

			err := ps.Update(context.Background(), "Epic__c", "epic-001", models.SObject{
				"Channel__c": value,
			}, recordEntrypointUser())
			if err == nil {
				t.Fatal("Update explicit blank required picklist = nil, want validation failure")
			}
			if !strings.Contains(err.Error(), "Channel__c") {
				t.Fatalf("Update error = %v, want Channel__c", err)
			}
			if !strings.Contains(err.Error(), "is required") {
				t.Fatalf("Update error = %v, want required-field rejection", err)
			}
			if repo.updateCalled {
				t.Fatal("repo.Update called for explicit blank required picklist")
			}
		})
	}
}

func TestPreparedUpdatePreservesUntouchedHistoricalBlankRequiredPicklist(t *testing.T) {
	repo := &rollupSummaryRecordRepo{old: models.SObject{
		constants.FieldID:   "epic-001",
		constants.FieldName: "Sprint 1",
		"Channel__c":        "",
	}}
	ps := newRollupSummaryTestService(jiraLikeRequiredPicklistRollupSchema(), repo)

	prepared, err := ps.PreviewBeforeUpdate(context.Background(), "Epic__c", "epic-001", models.SObject{
		constants.FieldName: "Sprint 2",
	}, nil, recordEntrypointUser())
	if err != nil {
		t.Fatalf("PreviewBeforeUpdate unrelated field with historical blank required picklist: %v", err)
	}
	if got := prepared.Record["Channel__c"]; got != "" {
		t.Fatalf("prepared Channel__c = %#v, want locked empty-string carrier", got)
	}
	if _, exists := prepared.EffectiveUpdates["Channel__c"]; exists {
		t.Fatalf("prepared effective updates = %#v, want untouched Channel__c omitted", prepared.EffectiveUpdates)
	}
	if err := ps.CommitPreparedUpdate(context.Background(), "Epic__c", "epic-001", prepared, nil, recordEntrypointUser()); err != nil {
		t.Fatalf("CommitPreparedUpdate with historical blank required picklist: %v", err)
	}
	if got := repo.updated[constants.FieldName]; got != "Sprint 2" {
		t.Fatalf("repo.Update Name = %#v, want Sprint 2", got)
	}
	if _, exists := repo.updated["Channel__c"]; exists {
		t.Fatalf("repo.Update payload = %#v, want untouched Channel__c omitted", repo.updated)
	}
}

func TestCommitPreparedUpdateRebasesDeltaOntoLatestLockedRecord(t *testing.T) {
	schema := jiraLikeRollupSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName:    constants.FieldLastModifiedDate,
		Type:       constants.FieldTypeDatetime,
		Createable: false,
		Updateable: false,
		IsSystem:   true,
	})
	makeFieldsMap(schema)
	repo := &rollupSummaryRecordRepo{old: models.SObject{
		constants.FieldID:               "epic-001",
		constants.FieldName:             "Sprint 1",
		constants.FieldLastModifiedDate: "2026-08-13T08:00:00Z",
	}}
	ps := newRollupSummaryTestService(schema, repo)

	prepared, err := ps.PreviewBeforeUpdate(context.Background(), "Epic__c", "epic-001", models.SObject{
		constants.FieldName: "Sprint 2",
	}, nil, recordEntrypointUser())
	if err != nil {
		t.Fatalf("PreviewBeforeUpdate: %v", err)
	}
	if got := prepared.Record[constants.FieldLastModifiedDate]; got != "2026-08-13T08:00:00Z" {
		t.Fatalf("prepared LastModifiedDate = %#v, want preview snapshot", got)
	}
	if _, exists := prepared.EffectiveUpdates[constants.FieldLastModifiedDate]; exists {
		t.Fatalf("prepared effective updates = %#v, want untouched audit field omitted", prepared.EffectiveUpdates)
	}

	// Simulate an independent writer between preview and commit. The prepared
	// full image is now stale, but the intended Name delta remains valid.
	repo.old[constants.FieldLastModifiedDate] = "2026-08-13T08:00:01Z"
	if err := ps.CommitPreparedUpdate(context.Background(), "Epic__c", "epic-001", prepared, nil, recordEntrypointUser()); err != nil {
		t.Fatalf("CommitPreparedUpdate after concurrent audit stamp: %v", err)
	}
	if got := repo.updated[constants.FieldName]; got != "Sprint 2" {
		t.Fatalf("repo.Update Name = %#v, want Sprint 2", got)
	}
	if got := repo.updated[constants.FieldLastModifiedDate]; got == "2026-08-13T08:00:00Z" {
		t.Fatalf("repo.Update payload = %#v, stale preview audit value was written back", repo.updated)
	}
}

func TestUpdateDerivedPreservesUntouchedHistoricalBlankRequiredPicklist(t *testing.T) {
	repo := &rollupSummaryRecordRepo{old: models.SObject{
		constants.FieldID:   "epic-001",
		constants.FieldName: "Sprint 1",
		"Channel__c":        "",
	}}
	ps := newRollupSummaryTestService(jiraLikeRequiredPicklistRollupSchema(), repo)

	if err := ps.UpdateDerived(context.Background(), "Epic__c", "epic-001", models.SObject{
		"StoryPointTotal__c": float64(8),
	}, recordEntrypointUser()); err != nil {
		t.Fatalf("UpdateDerived with historical blank required picklist: %v", err)
	}
	if got := repo.updated["StoryPointTotal__c"]; got != float64(8) {
		t.Fatalf("repo.Update StoryPointTotal__c = %#v, want 8", got)
	}
	if _, exists := repo.updated["Channel__c"]; exists {
		t.Fatalf("repo.Update payload = %#v, want untouched Channel__c omitted", repo.updated)
	}
}

func TestUpdateRejectsRollupSummaryUserWrite(t *testing.T) {
	repo := &rollupSummaryRecordRepo{
		old: models.SObject{
			constants.FieldID:   "epic-001",
			constants.FieldName: "Sprint 1",
		},
	}
	ps := newRollupSummaryTestService(jiraLikeRollupSchema(), repo)

	err := ps.Update(context.Background(), "Epic__c", "epic-001", models.SObject{
		"StoryPointTotal__c": float64(8),
	}, recordEntrypointUser())
	if err == nil {
		t.Fatal("Update = nil, want RollupSummary rejection")
	}
	if !strings.Contains(err.Error(), "RollupSummary") {
		t.Fatalf("Update error = %v, want RollupSummary rejection", err)
	}
	if repo.updateCalled {
		t.Fatal("repo.Update called for user RollupSummary write, want rejection before storage write")
	}
}

func TestBulkInsertPrunesRollupSummaryUserWriteAndExcludesInsertColumn(t *testing.T) {
	repo := &rollupSummaryRecordRepo{}
	ps := newRollupSummaryTestService(jiraLikeRollupSchema(), repo)

	result, err := ps.BulkInsert(context.Background(), "Epic__c", []models.SObject{{
		constants.FieldName:  "Sprint 1",
		"StoryPointTotal__c": float64(8),
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}
	if result.SuccessCount != 1 {
		t.Fatalf("BulkInsert success count = %d, want 1 (errors=%v)", result.SuccessCount, result.Errors)
	}
	if !repo.bulkInsertCalled {
		t.Fatal("expected BulkInsert to reach repo")
	}
	for _, col := range repo.bulkColumns {
		if col == "StoryPointTotal__c" {
			t.Fatalf("bulk insert columns = %v, want rollup field excluded", repo.bulkColumns)
		}
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulk inserted rows = %d, want 1", len(repo.bulkInserted))
	}
	if _, ok := repo.bulkInserted[0]["StoryPointTotal__c"]; ok {
		t.Fatalf("bulk inserted record = %#v, want rollup field pruned before repo write", repo.bulkInserted[0])
	}
}
