package records

import (
	"context"
	"errors"
	"testing"

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

func TestWrapRollupHookErrorPreservesValidationRuleError(t *testing.T) {
	ruleErr := appErrors.NewValidationRuleError("Rollup_Profile_Gate", "Description is required")

	got := wrapRollupHookError("failed to process rollups", ruleErr)
	if got == nil {
		t.Fatal("wrapRollupHookError = nil, want validation rule error")
	}

	var gotRule *appErrors.ValidationRuleError
	if !errors.As(got, &gotRule) {
		t.Fatalf("wrapRollupHookError = %T (%v), want ValidationRuleError", got, got)
	}
	if gotRule != ruleErr {
		t.Fatalf("wrapRollupHookError returned %#v, want original rule error", gotRule)
	}
	if appErrors.IsInternal(got) {
		t.Fatalf("wrapRollupHookError returned internal error %v, want validation classification", got)
	}
}

func TestWrapRollupHookErrorKeepsInternalErrorInternal(t *testing.T) {
	ruleErr := appErrors.NewValidationRuleError("Rollup_Profile_Gate", "Description is required")
	internal := appErrors.NewInternalError("rollback failed", ruleErr)

	got := wrapRollupHookError("failed to process rollups", internal)
	if got == nil {
		t.Fatal("wrapRollupHookError = nil, want internal error")
	}
	if !appErrors.IsInternal(got) {
		t.Fatalf("wrapRollupHookError = %T (%v), want internal classification", got, got)
	}
}

type rollupProfileRuleCalcRepo struct {
	value interface{}
}

type alwaysPessimisticRollupTestTxManager struct{}

func (alwaysPessimisticRollupTestTxManager) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	return runAlwaysPessimisticRollupTestTx(ctx, false, fn)
}
func (alwaysPessimisticRollupTestTxManager) WithPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return runAlwaysPessimisticRollupTestTx(ctx, true, fn)
}
func (alwaysPessimisticRollupTestTxManager) WithRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return runAlwaysPessimisticRollupTestTx(ctx, false, fn)
}
func (alwaysPessimisticRollupTestTxManager) WithPessimisticRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return runAlwaysPessimisticRollupTestTx(ctx, true, fn)
}

func runAlwaysPessimisticRollupTestTx(ctx context.Context, pessimistic bool, fn func(context.Context) error) error {
	txCtx := context.WithValue(ctx, inlineTxActiveKey{}, true)
	txCtx = context.WithValue(txCtx, inlineTxPessimisticKey{}, pessimistic)
	txCtx = context.WithValue(txCtx, inlineTxIdentityKey{}, &struct{}{})
	txCtx = ports.WithTransactionAbort(txCtx)
	txCtx = ports.WithTransactionCompletionHooks(txCtx)
	err := fn(txCtx)
	if err != nil {
		ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionRolledBack)
		return err
	}
	if abortErr := ports.PrepareTransactionCommit(txCtx); abortErr != nil {
		ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionRolledBack)
		return abortErr
	}
	ports.RunTransactionCompletionHooks(txCtx, ports.TransactionCompletionCommitted)
	return nil
}
func (alwaysPessimisticRollupTestTxManager) IsTransactionActive(context.Context) bool { return true }
func (alwaysPessimisticRollupTestTxManager) IsPessimisticTx(context.Context) bool     { return true }
func (alwaysPessimisticRollupTestTxManager) ExtractTx(context.Context) interface{}    { return nil }
func (alwaysPessimisticRollupTestTxManager) DetachTx(ctx context.Context) context.Context {
	return ctx
}

func (r *rollupProfileRuleCalcRepo) CalculateRollup(_ context.Context, _, _, _, _, _ string, _ *string) (interface{}, error) {
	return r.value, nil
}

func (r *rollupProfileRuleCalcRepo) CalculateRollupCurrentRead(_ context.Context, _, _, _, _, _ string, _ *string) (interface{}, error) {
	return r.value, nil
}

func (r *rollupProfileRuleCalcRepo) RecordMatchesRollupFilter(_ context.Context, _, _ string, _ *string) (bool, error) {
	return true, nil
}

func (r *rollupProfileRuleCalcRepo) GetParentIDsPage(_ context.Context, _ string, _ string, _ int) ([]string, error) {
	return nil, nil
}

func TestProcessRollupsOpportunityParentValidationUsesDerivedRecomputeMarker(t *testing.T) {
	const (
		parentObj   = "Opportunity"
		parentID    = "006-opp"
		childObj    = "OpportunityLineItem"
		rollupField = "Amount"
		childField  = "TotalPrice"
		relField    = "OpportunityId"
	)

	schema := opportunityRollupProfileRuleSchema(childObj, childField, relField)
	rules := []*models.ValidationRule{{
		ID:            "vr-opportunity-profile-gate",
		ObjectAPIName: parentObj,
		Name:          "Rollup_Profile_Gate",
		Active:        true,
		Condition:     `NOT($SystemContext.IsDerivedRecompute) && $Profile.Id != "` + constants.ProfileSystemAdmin + `" && Amount > 0 && ISBLANK(Description)`,
		ErrorMessage:  "Description is required for non-derived non-admin updates",
	}}

	repo := &rollupSummaryRecordRepo{old: models.SObject{
		constants.FieldID:   parentID,
		constants.FieldName: "Profile gated opportunity",
		rollupField:         float64(0),
	}}
	metaRepo := &stubUpsertMetaRepo{schema: schema, validationRules: rules}
	metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	ps := &PersistenceService{
		repo:                repo,
		rollup:              noopRollupProcessor{},
		metadata:            metaSvc,
		permissions:         permSvc,
		eventBus:            &eventFeature.NoOpEventBus{},
		validator:           metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator:     validation.NewSchemaValidator(),
		txManager:           inlineTxManager{},
		parentLaneAdmission: &fakeParentLaneAdmissionController{},
	}

	txm := alwaysPessimisticRollupTestTxManager{}
	rollupSvc := analyticsFeature.NewRollupService(
		&rollupProfileRuleCalcRepo{value: float64(100)},
		metaSvc,
		txm,
		logging.New(context.Background()),
	)
	rollupSvc.SetDerivedWriter(ps)

	err := txm.WithPessimisticTransaction(context.Background(), func(ctx context.Context) error {
		return rollupSvc.ProcessRollups(ctx, childObj, models.SObject{
			relField:   parentID,
			childField: float64(100),
		})
	})
	if err != nil {
		t.Fatalf("ProcessRollups returned %T %v, want nil with derived recompute marker", err, err)
	}
	if !repo.updateCalled {
		t.Fatal("repo.Update was not called, want derived rollup write to land")
	}
	if got := repo.updated[rollupField]; got != float64(100) {
		t.Fatalf("repo.Update Amount = %v, want 100", got)
	}
}

func TestDeleteChildRollupPreservesUntouchedHistoricalBlankRequiredPicklist(t *testing.T) {
	const (
		parentObj   = "Parent__c"
		parentID    = "parent-1"
		childObj    = "Child__c"
		childID     = "child-1"
		rollupField = "ChildAmount__c"
		childField  = "Amount__c"
		relField    = "ParentId__c"
		required    = "Channel__c"
	)

	parent := relationshipDeleteParentSchema()
	parent.Fields = append(parent.Fields,
		models.FieldMetadata{
			APIName:    required,
			Type:       constants.FieldTypePicklist,
			Createable: true,
			Updateable: true,
			Required:   true,
			Options:    models.PicklistOptionsFromStrings([]string{"Direct", "Partner"}),
		},
		models.FieldMetadata{
			APIName:    rollupField,
			Type:       constants.FieldTypeRollupSummary,
			Createable: false,
			Updateable: false,
			RollupConfig: &models.RollupConfig{
				CalcType:          "SUM",
				SummaryObject:     childObj,
				SummaryField:      childField,
				RelationshipField: relField,
			},
		},
	)
	makeFieldsMap(parent)

	child := relationshipDeleteChildSchema(models.FieldMetadata{
		APIName:     relField,
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{parentObj},
		Createable:  true,
		Updateable:  true,
	})
	child.Fields = append(child.Fields, models.FieldMetadata{
		APIName:    childField,
		Type:       constants.FieldTypeDouble,
		Createable: true,
		Updateable: true,
	})
	makeFieldsMap(child)

	repo := &relationshipDeleteRepo{records: map[string]models.SObject{
		parentObj: {
			constants.FieldID:        parentID,
			constants.FieldName:      "Parent",
			constants.FieldIsDeleted: false,
			required:                 "",
			rollupField:              float64(10),
		},
		childObj: {
			constants.FieldID:        childID,
			constants.FieldName:      "Child",
			constants.FieldIsDeleted: false,
			relField:                 parentID,
			childField:               float64(10),
		},
	}}
	ps := newRollupBoundaryTestService(repo, parent, child)
	txm := inlineTxManager{}
	ps.txManager = txm
	rollupSvc := analyticsFeature.NewRollupService(
		&rollupProfileRuleCalcRepo{value: float64(0)},
		ps.metadata,
		txm,
		logging.New(context.Background()),
	)
	rollupSvc.SetDerivedWriter(ps)
	ps.rollup = rollupSvc

	if err := ps.Delete(context.Background(), childObj, childID, recordEntrypointUser()); err != nil {
		t.Fatalf("Delete child with historical blank required parent picklist: %v", err)
	}

	for _, call := range repo.updateCalls {
		if call.table != parentObj {
			continue
		}
		if got := call.updates[rollupField]; got != float64(0) {
			t.Fatalf("parent rollup update = %#v, want %s=0", call.updates, rollupField)
		}
		if _, exists := call.updates[required]; exists {
			t.Fatalf("parent rollup update = %#v, want untouched %s omitted", call.updates, required)
		}
		return
	}
	t.Fatalf("parent rollup update missing; calls=%#v", repo.updateCalls)
}

func TestProcessRollupsProfileGateWithoutDerivedMarkerStillFails(t *testing.T) {
	const (
		parentObj   = "Opportunity"
		parentID    = "006-opp"
		childObj    = "OpportunityLineItem"
		rollupField = "Amount"
		childField  = "TotalPrice"
		relField    = "OpportunityId"
	)

	tests := []struct {
		name      string
		condition string
	}{
		{
			name:      "profile id",
			condition: `$Profile.Id != "` + constants.ProfileSystemAdmin + `" && Amount > 0 && ISBLANK(Description)`,
		},
		{
			name:      "profile name",
			condition: `$Profile.Name != "Renamed System Administrator" && Amount > 0 && ISBLANK(Description)`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			schema := opportunityRollupProfileRuleSchema(childObj, childField, relField)
			rules := []*models.ValidationRule{{
				ID:            "vr-opportunity-profile-gate",
				ObjectAPIName: parentObj,
				Name:          "Rollup_Profile_Gate",
				Active:        true,
				Condition:     tt.condition,
				ErrorMessage:  "Description is required for non-admin rollup updates",
			}}

			repo := &rollupSummaryRecordRepo{old: models.SObject{
				constants.FieldID:   parentID,
				constants.FieldName: "Profile gated opportunity",
				rollupField:         float64(0),
			}}
			metaRepo := &stubUpsertMetaRepo{schema: schema, validationRules: rules}
			metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
			permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
			ps := &PersistenceService{
				repo:                repo,
				rollup:              noopRollupProcessor{},
				metadata:            metaSvc,
				permissions:         permSvc,
				eventBus:            &eventFeature.NoOpEventBus{},
				validator:           metadataFeature.NewValidationService(formula.NewEngine()),
				schemaValidator:     validation.NewSchemaValidator(),
				txManager:           inlineTxManager{},
				parentLaneAdmission: &fakeParentLaneAdmissionController{},
			}

			txm := alwaysPessimisticRollupTestTxManager{}
			rollupSvc := analyticsFeature.NewRollupService(
				&rollupProfileRuleCalcRepo{value: float64(100)},
				metaSvc,
				txm,
				logging.New(context.Background()),
			)
			rollupSvc.SetDerivedWriter(ps)

			err := txm.WithPessimisticTransaction(context.Background(), func(ctx context.Context) error {
				return rollupSvc.ProcessRollups(ctx, childObj, models.SObject{
					relField:   parentID,
					childField: float64(100),
				})
			})
			if err == nil {
				t.Fatal("ProcessRollups returned nil, want validation rule failure without derived marker")
			}
			var ruleErr *appErrors.ValidationRuleError
			if !errors.As(err, &ruleErr) {
				t.Fatalf("ProcessRollups error = %T %v, want ValidationRuleError", err, err)
			}
			if repo.updateCalled {
				t.Fatal("repo.Update called after validation failure, want no storage write")
			}
		})
	}
}

func TestUpdateDerivedProfileGateWithoutDerivedMarkerStillFails(t *testing.T) {
	const (
		parentObj   = "Opportunity"
		parentID    = "006-opp"
		childObj    = "OpportunityLineItem"
		rollupField = "Amount"
		childField  = "TotalPrice"
		relField    = "OpportunityId"
	)

	profileName := "Renamed System Administrator"
	schema := opportunityRollupProfileRuleSchema(childObj, childField, relField)
	rules := []*models.ValidationRule{{
		ID:            "vr-opportunity-profile-gate",
		ObjectAPIName: parentObj,
		Name:          "Rollup_Profile_Gate",
		Active:        true,
		Condition:     `$Profile.Name != "` + profileName + `" && Amount > 0 && ISBLANK(Description)`,
		ErrorMessage:  "Description is required for non-admin rollup updates",
	}}

	repo := &rollupSummaryRecordRepo{old: models.SObject{
		constants.FieldID:   parentID,
		constants.FieldName: "Profile gated opportunity",
		rollupField:         float64(0),
	}}
	metaRepo := &stubUpsertMetaRepo{schema: schema, validationRules: rules}
	metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	ps := &PersistenceService{
		repo:            repo,
		rollup:          noopRollupProcessor{},
		metadata:        metaSvc,
		permissions:     permSvc,
		eventBus:        &eventFeature.NoOpEventBus{},
		validator:       metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator: validation.NewSchemaValidator(),
		txManager:       inlineTxManager{},
	}

	blankProfileUser := &models.UserSession{ID: constants.SystemUserID, Name: "Rollup Engine"}
	err := ps.UpdateDerived(context.Background(), parentObj, parentID, models.SObject{rollupField: float64(100)}, blankProfileUser)
	if err == nil {
		t.Fatal("UpdateDerived with blank-profile user returned nil, want validation rule failure")
	}
	var ruleErr *appErrors.ValidationRuleError
	if !errors.As(err, &ruleErr) {
		t.Fatalf("UpdateDerived error = %T %v, want ValidationRuleError", err, err)
	}
	if repo.updateCalled {
		t.Fatal("repo.Update called after validation failure, want no storage write")
	}
}

func TestLoadSystemUserSessionUsesInternalActorWithoutProfileIdentity(t *testing.T) {
	ps := &PersistenceService{}

	user, err := ps.LoadSystemUserSession(context.Background())
	if err != nil {
		t.Fatalf("LoadSystemUserSession() error = %v", err)
	}
	if user.ID != constants.SystemUserID {
		t.Fatalf("system user ID = %q, want %q", user.ID, constants.SystemUserID)
	}
	if user.ProfileName != "" {
		t.Fatalf("ProfileName = %q, want empty behavior profile", user.ProfileName)
	}
	if user.ProfileID != "" {
		t.Fatalf("ProfileID = %q, want empty behavior profile", user.ProfileID)
	}
	vars := formula.BuildFormulaUserContext(user)
	profile := vars["__Profile"].(map[string]interface{})
	if profile["Id"] != "" {
		t.Fatalf("$Profile.Id = %v, want empty internal actor profile", profile["Id"])
	}
	if profile["Name"] != "" {
		t.Fatalf("$Profile.Name = %v, want empty internal actor profile", profile["Name"])
	}
	formulaUser := vars["__User"].(map[string]interface{})
	if formulaUser["ProfileId"] != "" {
		t.Fatalf("$User.ProfileId = %v, want empty internal actor profile", formulaUser["ProfileId"])
	}
	if user.IsSystemAdmin {
		t.Fatal("IsSystemAdmin = true, want false for non-privileged system context actor")
	}
}

func TestBuildUserContextMarksDerivedRecomputeWithoutAdminBehavior(t *testing.T) {
	ps := &PersistenceService{}
	user := &models.UserSession{
		ID:   constants.SystemUserID,
		Name: constants.SystemUserName,
	}

	ctx := ps.buildUserContext(withDerivedRecompute(context.Background()), user)
	systemCtx, ok := ctx[constants.FormulaContextSystemContext].(map[string]interface{})
	if !ok {
		t.Fatalf("$SystemContext = %#v, want map", ctx[constants.FormulaContextSystemContext])
	}
	if systemCtx[constants.FormulaSystemContextIsDerivedRecompute] != true {
		t.Fatalf("$SystemContext.IsDerivedRecompute = %v, want true", systemCtx[constants.FormulaSystemContextIsDerivedRecompute])
	}
	profile := ctx["__Profile"].(map[string]interface{})
	if profile["Id"] != "" || profile["Name"] != "" {
		t.Fatalf("$Profile = %#v, want empty internal actor profile", profile)
	}
	if user.ProfileID != "" || user.ProfileName != "" || user.IsSystemAdmin {
		t.Fatalf("behavior session = %#v, want non-privileged internal actor", user)
	}

	normalCtx := ps.buildUserContext(context.Background(), user)
	normalSystemCtx := normalCtx[constants.FormulaContextSystemContext].(map[string]interface{})
	if normalSystemCtx[constants.FormulaSystemContextIsDerivedRecompute] != false {
		t.Fatalf("normal $SystemContext.IsDerivedRecompute = %v, want false", normalSystemCtx[constants.FormulaSystemContextIsDerivedRecompute])
	}
}

func opportunityRollupProfileRuleSchema(childObj, childField, relField string) *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Opportunity",
		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: "Description", Type: constants.FieldTypeTextarea, Createable: true, Updateable: true},
			{
				APIName:    "Amount",
				Type:       constants.FieldTypeRollupSummary,
				Createable: false,
				Updateable: false,
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      childField,
					RelationshipField: relField,
				},
			},
		},
	}
	makeFieldsMap(schema)
	return schema
}
