package analytics_test

import (
	"context"
	"fmt"
	"testing"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/features/analytics"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

// --- Mock RollupRepository ---

type mockRollupRepo struct {
	mock.Mock
}

func (m *mockRollupRepo) CalculateRollup(ctx context.Context, calcType, summaryObject, summaryField, relationField, parentID string, filter *string) (interface{}, error) {
	args := m.Called(ctx, calcType, summaryObject, summaryField, relationField, parentID, filter)
	return args.Get(0), args.Error(1)
}

func (m *mockRollupRepo) UpdateParentRollup(ctx context.Context, parentObjName, parentID, targetField string, value interface{}) error {
	args := m.Called(ctx, parentObjName, parentID, targetField, value)
	return args.Error(0)
}

func (m *mockRollupRepo) GetAllParentIDs(ctx context.Context, objectName string) ([]string, error) {
	args := m.Called(ctx, objectName)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]string), args.Error(1)
}

// --- Minimal Mock MetadataRepository for rollup tests ---
// Reuses patterns from query_service_test.go but simplified.

type rollupMetaRepo struct {
	schemas map[string]*models.ObjectMetadata
}

func newRollupMetaRepo() *rollupMetaRepo {
	return &rollupMetaRepo{schemas: make(map[string]*models.ObjectMetadata)}
}

func (m *rollupMetaRepo) GetAllSchemas(ctx context.Context) ([]*models.ObjectMetadata, error) {
	result := make([]*models.ObjectMetadata, 0, len(m.schemas))
	for _, s := range m.schemas {
		result = append(result, s)
	}
	return result, nil
}

func (m *rollupMetaRepo) GetSchemaByAPIName(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	if s, ok := m.schemas[apiName]; ok {
		return s, nil
	}
	return nil, nil
}

// Stubs for the rest of the MetadataRepository interface
func (m *rollupMetaRepo) GetFieldsForObject(ctx context.Context, objectID string) ([]models.FieldMetadata, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetRecordTypes(ctx context.Context, objectAPIName string) ([]*models.RecordType, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAutoNumbers(ctx context.Context, objectAPIName string) ([]*models.AutoNumber, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAllAutoNumbers(_ context.Context) ([]*models.AutoNumber, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAllIdentifierRules(_ context.Context) ([]*models.IdentifierRule, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAllValidationRules(_ context.Context) ([]*models.ValidationRule, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAllRelationships(_ context.Context) ([]*models.Relationship, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetRelationships(ctx context.Context, childObjectAPIName string) ([]*models.Relationship, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetFieldDependencies(ctx context.Context, objectAPIName string) ([]*models.FieldDependency, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetChildRelationships(ctx context.Context, parentObjectAPIName string) ([]*models.ObjectMetadata, error) {
	return nil, nil
}
func (m *rollupMetaRepo) ConfigureAutoNumberField(ctx context.Context, objectAPIName, fieldAPIName, format string, start int64) error {
	return nil
}
func (m *rollupMetaRepo) UpsertUIComponent(ctx context.Context, component *models.UIComponent) error {
	return nil
}
func (m *rollupMetaRepo) UpsertSetupPage(ctx context.Context, page *models.SetupPage) error {
	return nil
}
func (m *rollupMetaRepo) GetSetupPages(ctx context.Context) ([]*models.SetupPage, error) {
	return nil, nil
}
func (m *rollupMetaRepo) UpsertLayout(ctx context.Context, layout *models.PageLayout) error {
	return nil
}
func (m *rollupMetaRepo) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	return nil
}
func (m *mockRollupRepo) MarkLayoutCustomized(_ context.Context, _ string) {}
func (m *rollupMetaRepo) GetLayout(ctx context.Context, id string) (*models.PageLayout, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	return nil, nil
}
func (m *rollupMetaRepo) DeleteLayout(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) AssignLayoutToProfile(ctx context.Context, profileID, objectAPIName, layoutID string) error {
	return nil
}
func (m *rollupMetaRepo) GetLayoutIDForProfile(ctx context.Context, objectAPIName, profileID string) (string, error) {
	return "", nil
}
func (m *rollupMetaRepo) GetRelatedListConfigs(ctx context.Context, layoutObjectAPIName string) ([]ports.RelatedListConfig, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetActiveTheme(ctx context.Context) (*models.Theme, error) { return nil, nil }
func (m *rollupMetaRepo) GetThemeByName(ctx context.Context, name string) (*models.Theme, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateTheme(ctx context.Context, theme *models.Theme) error { return nil }
func (m *rollupMetaRepo) UpdateTheme(ctx context.Context, theme *models.Theme) error { return nil }
func (m *rollupMetaRepo) ActivateTheme(ctx context.Context, themeID string) error    { return nil }
func (m *rollupMetaRepo) GetAllApps(ctx context.Context) ([]*models.AppConfig, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetApp(ctx context.Context, id string) (*models.AppConfig, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAppWithTx(ctx context.Context, tx interface{}, id string) (*models.AppConfig, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateApp(ctx context.Context, app *models.AppConfig) error { return nil }
func (m *rollupMetaRepo) CreateAppMenuItem(ctx context.Context, app *models.AppConfig) error {
	return nil
}
func (m *rollupMetaRepo) UpdateApp(ctx context.Context, id string, app *models.AppConfig) error {
	return nil
}
func (m *rollupMetaRepo) UpdateAppWithTx(ctx context.Context, tx interface{}, id string, app *models.AppConfig) error {
	return nil
}
func (m *rollupMetaRepo) DeleteApp(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) GetAllDashboards(ctx context.Context) ([]*models.DashboardConfig, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetDashboard(ctx context.Context, id string) (*models.DashboardConfig, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateDashboard(ctx context.Context, dashboard *models.DashboardConfig) error {
	return nil
}
func (m *rollupMetaRepo) UpdateDashboard(ctx context.Context, id string, dashboard *models.DashboardConfig) error {
	return nil
}
func (m *rollupMetaRepo) DeleteDashboard(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) GetListViews(ctx context.Context, objectAPIName string) ([]*models.ListView, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateListView(ctx context.Context, lv *models.ListView) error { return nil }
func (m *rollupMetaRepo) UpdateListView(ctx context.Context, id string, lv *models.ListView) error {
	return nil
}
func (m *rollupMetaRepo) DeleteListView(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) CountListViews(ctx context.Context, objectAPIName string) (int, error) {
	return 0, nil
}
func (m *rollupMetaRepo) GetActions(ctx context.Context, objectAPIName string) ([]*models.ActionMetadata, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAction(ctx context.Context, id string) (*models.ActionMetadata, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAllActions(ctx context.Context) ([]*models.ActionMetadata, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateAction(ctx context.Context, action *models.ActionMetadata) error {
	return nil
}
func (m *rollupMetaRepo) UpdateAction(ctx context.Context, actionID string, updates *models.ActionMetadata) error {
	return nil
}
func (m *rollupMetaRepo) DeleteAction(ctx context.Context, actionID string) error { return nil }
func (m *rollupMetaRepo) CheckActionExists(ctx context.Context, objectAPIName, name string) (bool, error) {
	return false, nil
}
func (m *rollupMetaRepo) GetSharingRules(ctx context.Context, objectAPIName string) ([]*models.SharingRule, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetAllFlowDefinitions(ctx context.Context) ([]*models.FlowDefinition, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetFlowDefinition(ctx context.Context, id string) (*models.FlowDefinition, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetFlowDefinitionByName(ctx context.Context, developerName string) (*models.FlowDefinition, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateFlowDefinition(ctx context.Context, def *models.FlowDefinition) error {
	return nil
}
func (m *rollupMetaRepo) UpdateFlowDefinition(ctx context.Context, id string, def *models.FlowDefinition) error {
	return nil
}
func (m *rollupMetaRepo) PatchFlowDefinition(ctx context.Context, id string, patch *models.FlowDefinitionPatch) error {
	return nil
}
func (m *rollupMetaRepo) DeleteFlowDefinition(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) GetFlowVersions(ctx context.Context, definitionId string) ([]*models.FlowVersion, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetFlowVersionsByIDs(_ context.Context, _ []string) (map[string]*models.FlowVersion, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetFlowVersion(ctx context.Context, id string) (*models.FlowVersion, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetActiveFlowVersion(ctx context.Context, definitionId string) (*models.FlowVersion, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	return nil
}
func (m *rollupMetaRepo) CreateSeededFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	return nil
}
func (m *rollupMetaRepo) CreateTenantFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	return nil
}
func (m *rollupMetaRepo) UpdateFlowVersion(ctx context.Context, id string, version *models.FlowVersion) error {
	return nil
}
func (m *rollupMetaRepo) ActivateFlowVersion(ctx context.Context, id string) error   { return nil }
func (m *rollupMetaRepo) DeactivateFlowVersion(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) DeleteFlowVersion(ctx context.Context, id string) error     { return nil }
func (m *rollupMetaRepo) SetLatestVersionId(ctx context.Context, definitionId string, versionId string) error {
	return nil
}
func (m *rollupMetaRepo) SetTriggerInfo(ctx context.Context, definitionId string, triggerType string, objectOrEvent string) error {
	return nil
}
func (m *rollupMetaRepo) SetDefaultLayout(ctx context.Context, objectAPIName string, layoutID string, actorID *string) error {
	return nil
}
func (m *rollupMetaRepo) GetFlowInterviews(ctx context.Context) ([]*models.FlowInterview, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetFlowInterview(ctx context.Context, id string) (*models.FlowInterview, error) {
	return nil, nil
}
func (m *rollupMetaRepo) SaveFlowInterview(ctx context.Context, interview *models.FlowInterview) error {
	return nil
}
func (m *rollupMetaRepo) DeleteFlowInterview(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) GetValidationRules(ctx context.Context, objectAPIName string) ([]*models.ValidationRule, error) {
	return nil, nil
}
func (m *rollupMetaRepo) GetValidationRule(ctx context.Context, id string) (*models.ValidationRule, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateValidationRule(ctx context.Context, rule *models.ValidationRule) error {
	return nil
}
func (m *rollupMetaRepo) UpdateValidationRule(ctx context.Context, id string, updates *models.ValidationRule) error {
	return nil
}
func (m *rollupMetaRepo) DeleteValidationRule(ctx context.Context, id string) error { return nil }
func (m *rollupMetaRepo) CheckValidationRuleExists(ctx context.Context, objectAPIName, name string) (bool, error) {
	return false, nil
}

// --- Mock TransactionManager ---

type mockTxManager struct{}

func (m *mockTxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
	return fn(ctx)
}

func (m *mockTxManager) WithRetry(ctx context.Context, fn func(ctx context.Context) error, maxRetries int) error {
	return fn(ctx)
}

func (m *mockTxManager) IsTransactionActive(ctx context.Context) bool {
	return false
}

func (m *mockTxManager) ExtractTx(ctx context.Context) interface{} {
	return nil
}

func (m *mockTxManager) DetachTx(ctx context.Context) context.Context { return ctx }

// Helper to create RollupService with mock metadata
func newRollupTestService(schemas map[string]*models.ObjectMetadata, repo ports.RollupRepository, txMgr ports.TransactionManager) *analytics.RollupService {
	metaRepo := newRollupMetaRepo()
	for k, v := range schemas {
		metaRepo.schemas[k] = v
	}
	metaSvc := metadata.NewMetadataService(metaRepo, nil, nil)
	if txMgr == nil {
		txMgr = &mockTxManager{}
	}
	return analytics.NewRollupService(repo, metaSvc, txMgr, logging.New(context.Background()))
}

// --- FindAffectedRollups tests ---

func TestFindAffectedRollups(t *testing.T) {
	ctx := context.Background()

	t.Run("no rollup fields returns empty", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {APIName: "Account", Fields: []models.FieldMetadata{{APIName: "Name"}}},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{})
		assert.NoError(t, err)
		assert.Empty(t, affected)
	})

	t.Run("rollup field on different child object is skipped", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalCases",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Case",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "COUNT",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.NoError(t, err)
		assert.Empty(t, affected)
	})

	t.Run("matching rollup field found", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalOpps",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.NoError(t, err)
		require.Len(t, affected, 1)
		assert.Equal(t, "Account", affected[0].ParentObjName)
		assert.Equal(t, "acc-1", affected[0].ParentID)
	})

	t.Run("child record without parent ID is skipped", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalOpps",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{})
		assert.NoError(t, err)
		assert.Empty(t, affected)
	})

	t.Run("child record with nil parent ID is skipped", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalOpps",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{"AccountId": nil})
		assert.NoError(t, err)
		assert.Empty(t, affected)
	})

	t.Run("child record with empty string parent ID is skipped", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalOpps",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{"AccountId": ""})
		assert.NoError(t, err)
		assert.Empty(t, affected)
	})

	t.Run("rollup field with empty RelationshipField is skipped", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalOpps",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.NoError(t, err)
		assert.Empty(t, affected)
	})

	t.Run("multiple rollup fields on same parent", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalOppAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
					{
						APIName: "OppCount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Id",
							RelationshipField: "AccountId",
							CalcType:          "COUNT",
						},
					},
				},
			},
		}
		rs := newRollupTestService(schemas, nil, nil)

		affected, err := rs.FindAffectedRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.NoError(t, err)
		assert.Len(t, affected, 2)
	})
}

// --- ProcessRollups tests ---

func TestProcessRollups(t *testing.T) {
	ctx := context.Background()

	t.Run("no affected rollups does nothing", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {APIName: "Account", Fields: []models.FieldMetadata{{APIName: "Name"}}},
		}
		repo := new(mockRollupRepo)
		rs := newRollupTestService(schemas, repo, nil)

		err := rs.ProcessRollups(ctx, "SomeChild", models.SObject{})
		assert.NoError(t, err)
	})

	t.Run("calculates and updates parent", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		rs := newRollupTestService(schemas, repo, nil)

		repo.On("CalculateRollup", ctx, "SUM", "Opportunity", "Amount", "AccountId", "acc-1", (*string)(nil)).Return(float64(2500), nil)
		repo.On("UpdateParentRollup", ctx, "Account", "acc-1", "TotalAmount", float64(2500)).Return(nil)

		err := rs.ProcessRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.NoError(t, err)
		repo.AssertCalled(t, "UpdateParentRollup", ctx, "Account", "acc-1", "TotalAmount", float64(2500))
	})

	t.Run("calculate error propagates", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		rs := newRollupTestService(schemas, repo, nil)

		repo.On("CalculateRollup", ctx, "SUM", "Opportunity", "Amount", "AccountId", "acc-1", (*string)(nil)).Return(nil, fmt.Errorf("calc error"))

		err := rs.ProcessRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "failed to calculate rollup")
	})

	t.Run("update error propagates", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		rs := newRollupTestService(schemas, repo, nil)

		repo.On("CalculateRollup", ctx, "SUM", "Opportunity", "Amount", "AccountId", "acc-1", (*string)(nil)).Return(float64(100), nil)
		repo.On("UpdateParentRollup", ctx, "Account", "acc-1", "TotalAmount", float64(100)).Return(fmt.Errorf("update error"))

		err := rs.ProcessRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "failed to update parent rollup")
	})
}

// --- RecalculateAll tests ---

func TestRecalculateAll(t *testing.T) {
	ctx := context.Background()

	t.Run("object not found returns error", func(t *testing.T) {
		rs := newRollupTestService(nil, nil, nil)

		_, err := rs.RecalculateAll(ctx, "NonExistent")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "not found")
	})

	t.Run("no rollup fields returns zero", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {APIName: "Account", Fields: []models.FieldMetadata{{APIName: "Name"}}},
		}
		rs := newRollupTestService(schemas, nil, nil)

		count, err := rs.RecalculateAll(ctx, "Account")
		assert.NoError(t, err)
		assert.Equal(t, 0, count)
	})

	t.Run("no parent IDs returns zero", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		repo.On("GetAllParentIDs", ctx, "Account").Return([]string{}, nil)
		rs := newRollupTestService(schemas, repo, nil)

		count, err := rs.RecalculateAll(ctx, "Account")
		assert.NoError(t, err)
		assert.Equal(t, 0, count)
	})

	t.Run("recalculates and updates parents", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		txMgr := &mockTxManager{}

		repo.On("GetAllParentIDs", ctx, "Account").Return([]string{"acc-1", "acc-2"}, nil)
		repo.On("CalculateRollup", mock.Anything, "SUM", "Opportunity", "Amount", "AccountId", "acc-1", (*string)(nil)).Return(float64(100), nil)
		repo.On("CalculateRollup", mock.Anything, "SUM", "Opportunity", "Amount", "AccountId", "acc-2", (*string)(nil)).Return(float64(200), nil)
		repo.On("UpdateParentRollup", mock.Anything, "Account", "acc-1", "TotalAmount", float64(100)).Return(nil)
		repo.On("UpdateParentRollup", mock.Anything, "Account", "acc-2", "TotalAmount", float64(200)).Return(nil)

		rs := newRollupTestService(schemas, repo, txMgr)

		count, err := rs.RecalculateAll(ctx, "Account")
		assert.NoError(t, err)
		assert.Equal(t, 2, count)
	})

	t.Run("GetAllParentIDs error propagates", func(t *testing.T) {
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		repo.On("GetAllParentIDs", ctx, "Account").Return(nil, fmt.Errorf("db error"))

		rs := newRollupTestService(schemas, repo, nil)

		_, err := rs.RecalculateAll(ctx, "Account")
		assert.Error(t, err)
	})
}

// --- CalculateRollup tests (via ProcessRollups with specific filter scenarios) ---

func TestCalculateRollup_InvalidFilter(t *testing.T) {
	ctx := context.Background()

	t.Run("SQL injection in filter is blocked", func(t *testing.T) {
		filter := "status = 'a'; DROP TABLE users"
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "TotalAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
							Filter:            &filter,
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		rs := newRollupTestService(schemas, repo, nil)

		err := rs.ProcessRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "invalid filter expression")
	})

	t.Run("valid filter passes through to repo", func(t *testing.T) {
		filter := "status = 'active'"
		schemas := map[string]*models.ObjectMetadata{
			"Account": {
				APIName: "Account",
				Fields: []models.FieldMetadata{
					{
						APIName: "ActiveAmount",
						RollupConfig: &models.RollupConfig{
							SummaryObject:     "Opportunity",
							SummaryField:      "Amount",
							RelationshipField: "AccountId",
							CalcType:          "SUM",
							Filter:            &filter,
						},
					},
				},
			},
		}
		repo := new(mockRollupRepo)
		repo.On("CalculateRollup", ctx, "SUM", "Opportunity", "Amount", "AccountId", "acc-1", &filter).Return(float64(500), nil)
		repo.On("UpdateParentRollup", ctx, "Account", "acc-1", "ActiveAmount", float64(500)).Return(nil)

		rs := newRollupTestService(schemas, repo, nil)

		err := rs.ProcessRollups(ctx, "Opportunity", models.SObject{"AccountId": "acc-1"})
		assert.NoError(t, err)
	})
}

func (m *rollupMetaRepo) MarkLayoutCustomized(_ context.Context, _ string)               {}
func (m *rollupMetaRepo) MarkFlowDefinitionCustomized(_ context.Context, _ string) error { return nil }

// Approval Layout Methods
func (m *rollupMetaRepo) GetApprovalLayouts(ctx context.Context, objectAPIName string) ([]*models.ApprovalPageLayout, error) {
	return []*models.ApprovalPageLayout{}, nil
}

func (m *rollupMetaRepo) GetApprovalLayout(ctx context.Context, id string) (*models.ApprovalPageLayout, error) {
	return nil, nil
}

func (m *rollupMetaRepo) SaveApprovalLayout(ctx context.Context, layout *models.ApprovalPageLayout) error {
	return nil
}

func (m *rollupMetaRepo) DeleteApprovalLayout(ctx context.Context, id string) error {
	return nil
}

func (m *rollupMetaRepo) SetDefaultApprovalLayout(ctx context.Context, objectAPIName string, layoutID string) error {
	return nil
}

// --- Layout Assignment interface stubs (WP2) ---
func (m *rollupMetaRepo) FindBestAssignment(_ context.Context, _ string, _ []string, _ string, _ string) (*models.LayoutAssignment, error) {
	return nil, nil
}
func (m *rollupMetaRepo) CreateAssignment(_ context.Context, _ *models.LayoutAssignment) error {
	return nil
}
func (m *rollupMetaRepo) DeleteAssignment(_ context.Context, _ string) error { return nil }
func (m *rollupMetaRepo) GetAssignmentsForObject(_ context.Context, _ string) ([]*models.LayoutAssignment, error) {
	return nil, nil
}
func (m *rollupMetaRepo) DeleteAssignmentsByLayoutID(_ context.Context, _ string) error { return nil }
func (m *rollupMetaRepo) WriteLayoutHistory(_ context.Context, _ *models.PageLayoutHistoryEntry) error {
	return nil
}
func (m *rollupMetaRepo) GetLayoutHistoryMaxVersion(_ context.Context, _ string) (int, error) {
	return 0, nil
}
func (m *rollupMetaRepo) GetMigrationVersion(_ context.Context, _ string) (int, error) { return 0, nil }
func (m *rollupMetaRepo) SetMigrationVersion(_ context.Context, _ string, _ int, _ string) error {
	return nil
}

func (m *rollupMetaRepo) GetLayoutHistoryConfig(_ context.Context, _ string, _ int) (string, error) {
	return "", nil
}

func (m *rollupMetaRepo) PruneLayoutHistory(_ context.Context, _ string, _ int) error { return nil }
