package records

import (
	"context"
	"fmt"
	"testing"

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

// ---------------------------------------------------------------------------
// BulkInsert rollup delegation tests (issue #1193)
// ---------------------------------------------------------------------------

// capturingBulkRollupProcessor records ProcessBulkRollups calls for test assertions.
type capturingBulkRollupProcessor struct {
	noopRollupProcessor
	bulkCalls []capturedBulkRollupCall
}

type capturedBulkRollupCall struct {
	childObjName string
	childRecords []models.SObject
	landedRows   []bool
}

func (c *capturingBulkRollupProcessor) ProcessBulkRollups(_ context.Context, childObjName string, childRecords []models.SObject, landedRows []bool) error {
	c.bulkCalls = append(c.bulkCalls, capturedBulkRollupCall{
		childObjName: childObjName,
		childRecords: cloneSObjects(childRecords),
		landedRows:   append([]bool(nil), landedRows...),
	})
	return nil
}

// parentWithRollupSchema returns a parent object schema with a rollup summary
// field pointing at a child object via a relationship field.
func parentWithRollupSchemaForBulk() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Project__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:    "TotalBudget__c",
				Type:       constants.FieldTypeRollupSummary,
				Createable: false,
				Updateable: false,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     "Task__c",
					SummaryField:      "Budget__c",
					RelationshipField: "ProjectId__c",
				},
			},
		},
	}
	makeFieldsMap(schema)
	return schema
}

// childWithRollupParentSchemaForBulk returns a child object schema that has a
// relationship field pointing back to the parent.
func childWithRollupParentSchemaForBulk() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Task__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:     "ProjectId__c",
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{"Project__c"},
				Createable:  true,
				Updateable:  true,
			},
			{
				APIName:    "Budget__c",
				Type:       constants.FieldTypeDouble,
				Createable: true,
				Updateable: true,
			},
		},
	}
	makeFieldsMap(schema)
	return schema
}

// bulkInsertRollupRepo wraps rollupSummaryRecordRepo and also supports
// multi-schema metadata (parent + child) needed for rollup fanout tests.
type bulkInsertRollupRepoForBulk struct {
	rollupSummaryRecordRepo
	landedOverride []bool
}

func (r *bulkInsertRollupRepoForBulk) 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)
	if r.landedOverride != nil {
		return r.landedOverride, nil
	}
	landed := make([]bool, len(recs))
	for i := range landed {
		landed[i] = true
	}
	return landed, nil
}

func (r *bulkInsertRollupRepoForBulk) FindOne(ctx context.Context, objectName, id string) (models.SObject, error) {
	return models.SObject{constants.FieldID: id, constants.FieldName: "stub-parent"}, nil
}

func newBulkInsertRollupTestService(parentSchema, childSchema *models.ObjectMetadata, repo *bulkInsertRollupRepoForBulk, rollupProcessor ports.RollupProcessor) *PersistenceService {
	metaRepo := &stubUpsertMetaRepo{schemas: []*models.ObjectMetadata{parentSchema, childSchema}}
	metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	return &PersistenceService{
		repo:                repo,
		rollup:              rollupProcessor,
		metadata:            metaSvc,
		permissions:         permSvc,
		eventBus:            &eventFeature.NoOpEventBus{},
		validator:           metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator:     validation.NewSchemaValidator(),
		txManager:           inlineTxManager{},
		parentLaneAdmission: &fakeParentLaneAdmissionController{},
	}
}

// TestBulkInsertDelegatesToRollupProcessor verifies that BulkInsert delegates
// rollup processing to ps.rollup.ProcessBulkRollups with the correct arguments.
// This is the core delegation test for issue #1193.
func TestBulkInsertDelegatesToRollupProcessor(t *testing.T) {
	parentSchema := parentWithRollupSchemaForBulk()
	childSchema := childWithRollupParentSchemaForBulk()
	repo := &bulkInsertRollupRepoForBulk{}
	rollupCapture := &capturingBulkRollupProcessor{}
	ps := newBulkInsertRollupTestService(parentSchema, childSchema, repo, rollupCapture)

	children := []models.SObject{
		{constants.FieldName: "Task A", "ProjectId__c": "proj-001", "Budget__c": float64(10)},
		{constants.FieldName: "Task B", "ProjectId__c": "proj-001", "Budget__c": float64(20)},
	}

	_, err := ps.BulkInsert(context.Background(), "Task__c", children, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}

	if len(rollupCapture.bulkCalls) == 0 {
		t.Fatal("ProcessBulkRollups was not called — BulkInsert should delegate to rollup processor")
	}

	call := rollupCapture.bulkCalls[0]
	if call.childObjName != "Task__c" {
		t.Errorf("ProcessBulkRollups childObjName = %q, want Task__c", call.childObjName)
	}
	if len(call.childRecords) != 2 {
		t.Fatalf("ProcessBulkRollups childRecords len = %d, want 2", len(call.childRecords))
	}
	if len(call.landedRows) != 2 {
		t.Fatalf("ProcessBulkRollups landedRows len = %d, want 2", len(call.landedRows))
	}
	for i, landed := range call.landedRows {
		if !landed {
			t.Errorf("ProcessBulkRollups landedRows[%d] = false, want true (all rows landed)", i)
		}
	}
}

func TestBulkInsertBeforeCreateMutationFeedsRollupProcessor(t *testing.T) {
	parentSchema := parentWithRollupSchemaForBulk()
	childSchema := childWithRollupParentSchemaForBulk()
	repo := &bulkInsertRollupRepoForBulk{}
	rollupCapture := &capturingBulkRollupProcessor{}
	ps := newBulkInsertRollupTestService(parentSchema, childSchema, repo, rollupCapture)
	ps.eventBus = lifecycleSpikeEventBus{publish: func(_ context.Context, eventType sharedEvents.EventType, payload interface{}) error {
		if eventType != sharedEvents.RecordBeforeCreate {
			return nil
		}
		recordPayload, ok := payload.(eventFeature.RecordEventPayload)
		if !ok {
			t.Fatalf("RecordBeforeCreate payload type = %T, want RecordEventPayload", payload)
		}
		recordPayload.Record["Budget__c"] = float64(99)
		return nil
	}}

	result, err := ps.BulkInsert(context.Background(), "Task__c", []models.SObject{{
		constants.FieldName: "Task A",
		"ProjectId__c":      "proj-001",
		"Budget__c":         float64(10),
	}}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}
	if result.SuccessCount != 1 || result.FailedCount != 0 {
		t.Fatalf("BulkInsert result = %+v, want one success", result)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("repo.BulkInsert rows = %d, want 1", len(repo.bulkInserted))
	}
	if got := repo.bulkInserted[0]["Budget__c"]; got != float64(99) {
		t.Fatalf("repo.BulkInsert Budget__c = %v, want before-save value 99", got)
	}
	if len(rollupCapture.bulkCalls) != 1 {
		t.Fatalf("ProcessBulkRollups calls = %d, want 1", len(rollupCapture.bulkCalls))
	}
	if got := rollupCapture.bulkCalls[0].childRecords[0]["Budget__c"]; got != float64(99) {
		t.Fatalf("ProcessBulkRollups child Budget__c = %v, want before-save value 99", got)
	}
}

func TestBulkInsertBeforeCreateMaterializesOpportunityLineItemRollupSources(t *testing.T) {
	parentSchema := &models.ObjectMetadata{
		APIName:      "Opportunity",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{
				APIName:    "Net_Fees__c",
				Type:       constants.FieldTypeRollupSummary,
				Createable: false,
				Updateable: false,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     "OpportunityLineItem",
					SummaryField:      "Net_fees__c",
					RelationshipField: "OpportunityId",
				},
			},
		},
	}
	makeFieldsMap(parentSchema)
	childSchema := &models.ObjectMetadata{
		APIName:      "OpportunityLineItem",
		SharingModel: constants.SharingModelControlledByParent,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{
				APIName:        "OpportunityId",
				Type:           constants.FieldTypeMasterDetail,
				IsMasterDetail: true,
				ReferenceTo:    []string{"Opportunity"},
				Createable:     true,
				Updateable:     true,
			},
			{APIName: "ListPrice", Type: constants.FieldTypeCurrency, Createable: false, Updateable: false},
			{APIName: "Unit_Net_Price__c", Type: constants.FieldTypeCurrency, Createable: true, Updateable: true},
			{APIName: "Quantity", Type: constants.FieldTypeDouble, Createable: true, Updateable: true},
			{APIName: "Term_Mounths__c", Type: constants.FieldTypeDouble, Createable: true, Updateable: true},
			{APIName: "Net_fees__c", Type: constants.FieldTypeCurrency, Createable: false, Updateable: false},
		},
	}
	makeFieldsMap(childSchema)
	repo := &bulkInsertRollupRepoForBulk{}
	rollupCapture := &capturingBulkRollupProcessor{}
	ps := newBulkInsertRollupTestService(parentSchema, childSchema, repo, rollupCapture)
	ps.eventBus = lifecycleSpikeEventBus{publish: func(_ context.Context, eventType sharedEvents.EventType, payload interface{}) error {
		if eventType != sharedEvents.RecordBeforeCreate {
			return nil
		}
		recordPayload, ok := payload.(eventFeature.RecordEventPayload)
		if !ok {
			t.Fatalf("RecordBeforeCreate payload type = %T, want RecordEventPayload", payload)
		}
		unitNet, _ := recordPayload.Record["Unit_Net_Price__c"].(float64)
		quantity, _ := recordPayload.Record["Quantity"].(float64)
		term, _ := recordPayload.Record["Term_Mounths__c"].(float64)
		recordPayload.Record["ListPrice"] = float64(36000)
		recordPayload.Record["Net_fees__c"] = unitNet * quantity * term
		return nil
	}}

	result, err := ps.BulkInsert(context.Background(), "OpportunityLineItem", []models.SObject{
		{"OpportunityId": "opp-001", "Unit_Net_Price__c": float64(24168), "Quantity": float64(1), "Term_Mounths__c": float64(12)},
		{"OpportunityId": "opp-001", "Unit_Net_Price__c": float64(26250), "Quantity": float64(1), "Term_Mounths__c": float64(12)},
		{"OpportunityId": "opp-001", "Unit_Net_Price__c": float64(28086), "Quantity": float64(1), "Term_Mounths__c": float64(12)},
	}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}
	if result.SuccessCount != 3 || result.FailedCount != 0 {
		t.Fatalf("BulkInsert result = %+v, want three successes", result)
	}
	if len(repo.bulkInserted) != 3 {
		t.Fatalf("repo.BulkInsert rows = %d, want 3", len(repo.bulkInserted))
	}
	if len(rollupCapture.bulkCalls) != 1 {
		t.Fatalf("ProcessBulkRollups calls = %d, want 1", len(rollupCapture.bulkCalls))
	}
	wantNetFees := []float64{290016, 315000, 337032}
	for i, want := range wantNetFees {
		if got := repo.bulkInserted[i]["ListPrice"]; got != float64(36000) {
			t.Fatalf("repo.BulkInsert row %d ListPrice = %v, want 36000", i, got)
		}
		if got := repo.bulkInserted[i]["Net_fees__c"]; got != want {
			t.Fatalf("repo.BulkInsert row %d Net_fees__c = %v, want %v", i, got, want)
		}
		if got := rollupCapture.bulkCalls[0].childRecords[i]["Net_fees__c"]; got != want {
			t.Fatalf("ProcessBulkRollups row %d Net_fees__c = %v, want %v", i, got, want)
		}
	}
}

// TestBulkInsertRollupPreservesLandingVector verifies that the landedRows
// vector (INSERT IGNORE evidence) is passed through to the rollup processor
// unchanged. When some rows are INSERT IGNORE (landed=false), those rows must
// be excluded from rollup fanout.
func TestBulkInsertRollupPreservesLandingVector(t *testing.T) {
	parentSchema := parentWithRollupSchemaForBulk()
	childSchema := childWithRollupParentSchemaForBulk()
	repo := &bulkInsertRollupRepoForBulk{
		landedOverride: []bool{true, false, true},
	}
	rollupCapture := &capturingBulkRollupProcessor{}
	ps := newBulkInsertRollupTestService(parentSchema, childSchema, repo, rollupCapture)

	_, err := ps.BulkInsert(context.Background(), "Task__c", []models.SObject{
		{constants.FieldName: "Task A", "ProjectId__c": "proj-001", "Budget__c": float64(10)},
		{constants.FieldName: "Task B", "ProjectId__c": "proj-001", "Budget__c": float64(20)},
		{constants.FieldName: "Task C", "ProjectId__c": "proj-002", "Budget__c": float64(30)},
	}, recordEntrypointUser(), BulkInsertOptions{IgnoreDuplicates: true})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}

	if len(rollupCapture.bulkCalls) == 0 {
		t.Fatal("ProcessBulkRollups was not called")
	}

	call := rollupCapture.bulkCalls[0]
	// The landing vector must be passed through exactly.
	if len(call.landedRows) != 3 {
		t.Fatalf("landedRows len = %d, want 3", len(call.landedRows))
	}
	if !call.landedRows[0] || call.landedRows[1] || !call.landedRows[2] {
		t.Errorf("landedRows = %v, want [true, false, true]", call.landedRows)
	}
}

// TestBulkInsertRollupPreservesResultCounts verifies that adding rollup
// fanout does not change the BulkInsertResult success/failure counts.
func TestBulkInsertRollupPreservesResultCounts(t *testing.T) {
	parentSchema := parentWithRollupSchemaForBulk()
	childSchema := childWithRollupParentSchemaForBulk()
	repo := &bulkInsertRollupRepoForBulk{}
	rollupCapture := &capturingBulkRollupProcessor{}
	ps := newBulkInsertRollupTestService(parentSchema, childSchema, repo, rollupCapture)

	result, err := ps.BulkInsert(context.Background(), "Task__c", []models.SObject{
		{constants.FieldName: "Task A", "ProjectId__c": "proj-001", "Budget__c": float64(10)},
		{constants.FieldName: "Task B", "ProjectId__c": "proj-002", "Budget__c": float64(20)},
	}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}

	if result.SuccessCount != 2 {
		t.Fatalf("BulkInsert success count = %d, want 2 (rollup fanout must not alter result counts)", result.SuccessCount)
	}
	if result.FailedCount != 0 {
		t.Fatalf("BulkInsert failed count = %d, want 0", result.FailedCount)
	}
}

// TestBulkInsertNoRollupWhenNoReferenceFields verifies that when a child
// object has no reference fields, BulkInsert does not force landing
// just for rollup purposes.
func TestBulkInsertNoRollupWhenNoReferenceFields(t *testing.T) {
	// Child schema with NO reference fields — no rollup possible
	childSchema := &models.ObjectMetadata{
		APIName:      "SimpleObj__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},
		},
	}
	makeFieldsMap(childSchema)

	repo := &bulkInsertRollupRepoForBulk{}
	rollupCapture := &capturingBulkRollupProcessor{}
	ps := newBulkInsertRollupTestService(childSchema, childSchema, repo, rollupCapture)

	result, err := ps.BulkInsert(context.Background(), "SimpleObj__c", []models.SObject{
		{constants.FieldName: fmt.Sprintf("Item %d", 1)},
	}, recordEntrypointUser(), BulkInsertOptions{})
	if err != nil {
		t.Fatalf("BulkInsert: %v", err)
	}

	if result.SuccessCount != 1 {
		t.Fatalf("expected 1 success, got %d", result.SuccessCount)
	}

	// No rollup calls expected — no reference fields
	if len(rollupCapture.bulkCalls) != 0 {
		t.Fatalf("expected 0 ProcessBulkRollups calls (no reference fields), got %d", len(rollupCapture.bulkCalls))
	}
}
