package analytics

import (
	"context"
	"testing"

	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestAffectedParentRowsForUpdate_BenignUpdateSkipsParentLockAndProcessing(t *testing.T) {
	const (
		parentObj   = "Parent__c"
		childObj    = "Child__c"
		parentID    = "parent-001"
		rollupField = "TotalAmount__c"
		relField    = "ParentId__c"
	)

	parentSchema := rollupUpdateParentSchema(parentObj, models.FieldMetadata{
		APIName: rollupField,
		Type:    constants.FieldTypeRollupSummary,
		RollupConfig: &models.RollupConfig{
			CalcType:          "SUM",
			SummaryObject:     childObj,
			SummaryField:      "Amount__c",
			RelationshipField: relField,
		},
	})
	childSchema := rollupUpdateChildSchema(childObj)
	meta := newFakeMetadata(map[string]*models.ObjectMetadata{parentObj: parentSchema, childObj: childSchema})
	repo := &stubRollupCalcRepo{calcValue: float64(42)}
	dw := &stubDerivedWriteCapture{}
	svc := NewRollupService(repo, meta, recordingRollupTxManager{active: true, pessimistic: true}, noopLogger())
	svc.SetDerivedWriter(dw)

	oldRecord := models.SObject{
		constants.FieldID:             "child-001",
		constants.FieldName:           "Old Name",
		constants.FieldIsDeleted:      false,
		"DisplayName__c":              "Old Display",
		relField:                      parentID,
		"Amount__c":                   float64(100),
		constants.FieldSystemModstamp: "stamp-1",
	}
	finalRecord := cloneAnalyticsTestSObject(oldRecord)
	finalRecord[constants.FieldName] = "New Name"
	finalRecord["DisplayName__c"] = "New Display"

	keys, err := svc.AffectedParentRowsForUpdate(context.Background(), childObj, finalRecord, oldRecord)
	if err != nil {
		t.Fatalf("AffectedParentRowsForUpdate: %v", err)
	}
	if len(keys) != 0 {
		t.Fatalf("AffectedParentRowsForUpdate keys = %#v, want none for benign update", keys)
	}

	if err := svc.ProcessRollupsForUpdate(context.Background(), childObj, finalRecord, oldRecord); err != nil {
		t.Fatalf("ProcessRollupsForUpdate: %v", err)
	}
	if len(dw.locks) != 0 || len(dw.calls) != 0 || repo.currentReadCalls != 0 || repo.calculateCalls != 0 {
		t.Fatalf("rollup side effects locks=%d updates=%d currentRead=%d calculate=%d, want all zero",
			len(dw.locks), len(dw.calls), repo.currentReadCalls, repo.calculateCalls)
	}
}

func TestAffectedParentRowsForUpdate_RelevanceMatrix(t *testing.T) {
	const (
		parentObj = "Parent__c"
		childObj  = "Child__c"
		parentID  = "parent-001"
		oldParent = "parent-old"
		newParent = "parent-new"
		relField  = "ParentId__c"
	)

	tests := []struct {
		name        string
		rollupField models.FieldMetadata
		oldRecord   models.SObject
		finalRecord models.SObject
		wantKeys    []recordlocks.ParentKey
	}{
		{
			name: "non-count summary source changed locks parent",
			rollupField: models.FieldMetadata{
				APIName: "TotalAmount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Amount__c": float64(11)}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "count ignores ordinary source-like field change",
			rollupField: models.FieldMetadata{
				APIName: "ChildCount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "COUNT",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Amount__c": float64(11)}),
			wantKeys:    nil,
		},
		{
			name: "relationship reparent locks old and new parents",
			rollupField: models.FieldMetadata{
				APIName: "TotalAmount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(oldParent, map[string]interface{}{"Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(newParent, map[string]interface{}{"Amount__c": float64(10)}),
			wantKeys: []recordlocks.ParentKey{
				{ObjectName: parentObj, RecordID: oldParent},
				{ObjectName: parentObj, RecordID: newParent},
			},
		},
		{
			name: "filter field changed locks parent",
			rollupField: models.FieldMetadata{
				APIName: "FilteredTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("Status__c = 'Open'"),
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Status__c": "Open", "Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Status__c": "Closed", "Amount__c": float64(10)}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "count backticked SQL filter field changed locks same parent",
			rollupField: models.FieldMetadata{
				APIName: "ActiveChildCount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "COUNT",
					SummaryObject:     childObj,
					SummaryField:      "*",
					RelationshipField: relField,
					Filter:            stringPtr("`Status__c` = 'Active'"),
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Status__c": "Inactive"}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Status__c": "Active"}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "count backticked SQL filter unrelated field skips same parent",
			rollupField: models.FieldMetadata{
				APIName: "ActiveChildCount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "COUNT",
					SummaryObject:     childObj,
					SummaryField:      "*",
					RelationshipField: relField,
					Filter:            stringPtr("`Status__c` = 'Active'"),
				},
			},
			oldRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Status__c":      "Inactive",
				"Description__c": "old",
			}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Status__c":      "Inactive",
				"Description__c": "new",
			}),
			wantKeys: nil,
		},
		{
			name: "count backticked SQL filter with unterminated single quote fails closed",
			rollupField: models.FieldMetadata{
				APIName: "ActiveChildCount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "COUNT",
					SummaryObject:     childObj,
					SummaryField:      "*",
					RelationshipField: relField,
					Filter:            stringPtr("`Status__c` = 'Active"),
				},
			},
			oldRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Status__c":      "Inactive",
				"Description__c": "old",
			}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Status__c":      "Inactive",
				"Description__c": "new",
			}),
			wantKeys: []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "count backticked SQL filter with unterminated double quote fails closed",
			rollupField: models.FieldMetadata{
				APIName: "ActiveChildCount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "COUNT",
					SummaryObject:     childObj,
					SummaryField:      "*",
					RelationshipField: relField,
					Filter:            stringPtr("`Status__c` = \"Active"),
				},
			},
			oldRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Status__c":      "Inactive",
				"Description__c": "old",
			}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Status__c":      "Inactive",
				"Description__c": "new",
			}),
			wantKeys: []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "unrelated field with clean filter dependencies skips parent",
			rollupField: models.FieldMetadata{
				APIName: "FilteredTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("Status__c = 'Open'"),
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Status__c": "Open", "Amount__c": float64(10), "Description__c": "a"}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Status__c": "Open", "Amount__c": float64(10), "Description__c": "b"}),
			wantKeys:    nil,
		},
		{
			name: "unknown filter dependencies fail closed",
			rollupField: models.FieldMetadata{
				APIName: "FilteredTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("Status__c = "),
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "a", "Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "b", "Amount__c": float64(10)}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "system generated filter dependency fails closed",
			rollupField: models.FieldMetadata{
				APIName: "RecentTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("LastModifiedDate != nil"),
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "a", "Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "b", "Amount__c": float64(10)}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "volatile NOW filter function fails closed",
			rollupField: models.FieldMetadata{
				APIName: "DueTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("DueDate__c < NOW()"),
				},
			},
			oldRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"DueDate__c":     "2026-07-07",
				"Description__c": "a",
				"Amount__c":      float64(10),
			}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"DueDate__c":     "2026-07-07",
				"Description__c": "b",
				"Amount__c":      float64(10),
			}),
			wantKeys: []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "volatile CURDATE filter function fails closed",
			rollupField: models.FieldMetadata{
				APIName: "ClosedTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("CloseDate__c <= CURDATE()"),
				},
			},
			oldRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"CloseDate__c":   "2026-07-07",
				"Description__c": "a",
				"Amount__c":      float64(10),
			}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"CloseDate__c":   "2026-07-07",
				"Description__c": "b",
				"Amount__c":      float64(10),
			}),
			wantKeys: []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "unsupported RAND filter function fails closed",
			rollupField: models.FieldMetadata{
				APIName: "RandomTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
					Filter:            stringPtr("Amount__c > RAND() * 100"),
				},
			},
			oldRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Description__c": "a",
				"Amount__c":      float64(10),
			}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{
				"Description__c": "b",
				"Amount__c":      float64(10),
			}),
			wantKeys: []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "system generated LastModifiedDate summary source fails closed",
			rollupField: models.FieldMetadata{
				APIName: "LatestModified__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "MAX",
					SummaryObject:     childObj,
					SummaryField:      constants.FieldLastModifiedDate,
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "a"}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "b"}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "system generated SystemModstamp summary source fails closed",
			rollupField: models.FieldMetadata{
				APIName: "LatestSystemStamp__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "MAX",
					SummaryObject:     childObj,
					SummaryField:      constants.FieldSystemModstamp,
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "a"}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "b"}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "system generated LastModifiedById summary source fails closed",
			rollupField: models.FieldMetadata{
				APIName: "LatestModifier__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "MAX",
					SummaryObject:     childObj,
					SummaryField:      constants.FieldLastModifiedByID,
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "a"}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "b"}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "active formula summary source fails closed",
			rollupField: models.FieldMetadata{
				APIName: "FormulaTotal__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "FormulaAmount__c",
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "a"}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{"Description__c": "b"}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
		{
			name: "delete membership change locks parent",
			rollupField: models.FieldMetadata{
				APIName: "TotalAmount__c",
				Type:    constants.FieldTypeRollupSummary,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount__c",
					RelationshipField: relField,
				},
			},
			oldRecord:   rollupUpdateRecord(parentID, map[string]interface{}{constants.FieldIsDeleted: false, "Amount__c": float64(10)}),
			finalRecord: rollupUpdateRecord(parentID, map[string]interface{}{constants.FieldIsDeleted: true, "Amount__c": float64(10)}),
			wantKeys:    []recordlocks.ParentKey{{ObjectName: parentObj, RecordID: parentID}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parentSchema := rollupUpdateParentSchema(parentObj, tt.rollupField)
			childSchema := rollupUpdateChildSchema(childObj)
			meta := newFakeMetadata(map[string]*models.ObjectMetadata{parentObj: parentSchema, childObj: childSchema})
			svc := NewRollupService(&stubRollupCalcRepo{}, meta, recordingRollupTxManager{active: true, pessimistic: true}, noopLogger())
			svc.SetDerivedWriter(&stubDerivedWriteCapture{})

			keys, err := svc.AffectedParentRowsForUpdate(context.Background(), childObj, tt.finalRecord, tt.oldRecord)
			if err != nil {
				t.Fatalf("AffectedParentRowsForUpdate: %v", err)
			}
			assertParentKeys(t, keys, tt.wantKeys)
		})
	}
}

func rollupUpdateParentSchema(apiName string, fields ...models.FieldMetadata) *models.ObjectMetadata {
	return &models.ObjectMetadata{
		APIName: apiName,
		Fields:  fields,
	}
}

func rollupUpdateChildSchema(apiName string) *models.ObjectMetadata {
	activeMode := "active"
	return &models.ObjectMetadata{
		APIName: apiName,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString},
			{APIName: constants.FieldName, Type: constants.FieldTypeString},
			{APIName: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean},
			{APIName: constants.FieldLastModifiedDate, Type: constants.FieldTypeDatetime},
			{APIName: constants.FieldLastModifiedByID, Type: constants.FieldTypeReference},
			{APIName: constants.FieldSystemModstamp, Type: constants.FieldTypeDatetime},
			{APIName: "ParentId__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}},
			{APIName: "Amount__c", Type: constants.FieldTypeDouble},
			{APIName: "DueDate__c", Type: constants.FieldTypeDate},
			{APIName: "CloseDate__c", Type: constants.FieldTypeDate},
			{APIName: "FormulaAmount__c", Type: constants.FieldTypeFormula, Formula: stringPtr("Amount__c * 2"), FormulaMode: &activeMode},
			{APIName: "Status__c", Type: constants.FieldTypeString},
			{APIName: "Description__c", Type: constants.FieldTypeString},
			{APIName: "DisplayName__c", Type: constants.FieldTypeString},
		},
	}
}

func rollupUpdateRecord(parentID string, extra map[string]interface{}) models.SObject {
	record := models.SObject{
		constants.FieldID:        "child-001",
		constants.FieldName:      "Child",
		constants.FieldIsDeleted: false,
		"ParentId__c":            parentID,
	}
	for key, value := range extra {
		record[key] = value
	}
	return record
}

func cloneAnalyticsTestSObject(in models.SObject) models.SObject {
	out := make(models.SObject, len(in))
	for key, value := range in {
		out[key] = value
	}
	return out
}

func assertParentKeys(t *testing.T, got, want []recordlocks.ParentKey) {
	t.Helper()
	got = recordlocks.OrderedParentKeys(got)
	want = recordlocks.OrderedParentKeys(want)
	if len(got) != len(want) {
		t.Fatalf("keys len = %d, want %d: got=%#v want=%#v", len(got), len(want), got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("key[%d] = %#v, want %#v; got=%#v", i, got[i], want[i], got)
		}
	}
}
