package analytics

import (
	"context"
	"errors"
	"testing"

	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/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// ---------------------------------------------------------------------------
// Minimal MetadataRepository stub — only GetAllSchemas and GetAllRelationships
// are called during MetadataService core-cache init; the embedded interface
// traps any other call with a nil panic.
// ---------------------------------------------------------------------------

type stubRollupMetaRepo struct {
	ports.MetadataRepository

	schemas []*models.ObjectMetadata
}

func (r *stubRollupMetaRepo) GetAllSchemas(_ context.Context) ([]*models.ObjectMetadata, error) {
	return r.schemas, nil
}
func (r *stubRollupMetaRepo) GetAllRelationships(_ context.Context) ([]*models.Relationship, error) {
	return nil, nil
}
func (r *stubRollupMetaRepo) GetSchemaByAPIName(_ context.Context, _ string) (*models.ObjectMetadata, error) {
	return nil, nil
}

// ---------------------------------------------------------------------------
// Inline stubs for RollupRepository and DerivedWriter
// ---------------------------------------------------------------------------

type stubRollupCalcRepo struct {
	calcValue            interface{}
	eventSink            *[]string
	parentIDs            []string
	calculateCalls       int
	currentReadCalls     int
	recordMatchByFilter  map[string]bool
	calculateErrAtCall   int
	calculateErr         error
	currentReadErrAtCall int
	currentReadErr       error
}

func (s *stubRollupCalcRepo) CalculateRollup(_ context.Context, _, _, _, _, _ string, _ *string) (interface{}, error) {
	s.calculateCalls++
	if s.eventSink != nil {
		*s.eventSink = append(*s.eventSink, "calc")
	}
	if s.calculateErr != nil && (s.calculateErrAtCall == 0 || s.calculateCalls == s.calculateErrAtCall) {
		return nil, s.calculateErr
	}
	return s.calcValue, nil
}
func (s *stubRollupCalcRepo) CalculateRollupCurrentRead(_ context.Context, _, _, _, _, _ string, _ *string) (interface{}, error) {
	s.currentReadCalls++
	if s.eventSink != nil {
		*s.eventSink = append(*s.eventSink, "calc")
	}
	if s.currentReadErr != nil && (s.currentReadErrAtCall == 0 || s.currentReadCalls == s.currentReadErrAtCall) {
		return nil, s.currentReadErr
	}
	return s.calcValue, nil
}
func (s *stubRollupCalcRepo) RecordMatchesRollupFilter(_ context.Context, _, _ string, filter *string) (bool, error) {
	if filter == nil {
		return true, nil
	}
	return s.recordMatchByFilter[*filter], nil
}
func (s *stubRollupCalcRepo) GetParentIDsPage(_ context.Context, _ string, afterID string, limit int) ([]string, error) {
	return buildPage(s.parentIDs, afterID, limit), nil
}

type stubDerivedWriteCapture struct {
	calls     []capturedDerivedWrite
	locks     []capturedDerivedLock
	events    []string
	eventSink *[]string
	rows      map[string]models.SObject
	enqueues  []capturedRollupDeltaEnqueue
	err       error
	loadErr   error
	user      *models.UserSession
	eventual  bool
}
type capturedDerivedWrite struct {
	objectName string
	recordID   string
	updates    models.SObject
	user       *models.UserSession
}
type capturedDerivedLock struct {
	objectName string
	recordID   string
}
type capturedRollupDeltaEnqueue struct {
	sourceObject string
	sourceID     string
	parentObject string
	parentID     string
	deltas       map[string]int64
}

func (s *stubDerivedWriteCapture) UpdateDerived(_ context.Context, objectName, recordID string, updates models.SObject, user *models.UserSession) error {
	if s.eventSink != nil {
		*s.eventSink = append(*s.eventSink, "update")
	}
	s.events = append(s.events, "update")
	s.calls = append(s.calls, capturedDerivedWrite{objectName: objectName, recordID: recordID, updates: updates, user: user})
	return s.err
}

func (s *stubDerivedWriteCapture) UpdateDerivedCountRollupValues(_ context.Context, objectName, recordID string, _ []string, user *models.UserSession, recalculate func(context.Context) (models.SObject, error)) error {
	updates, err := recalculate(context.Background())
	if err != nil {
		return err
	}
	return s.UpdateDerived(context.Background(), objectName, recordID, updates, user)
}

func (s *stubDerivedWriteCapture) CanApplyDerivedCountRollupDeltas(context.Context, string, ...string) (bool, error) {
	return true, nil
}

func (s *stubDerivedWriteCapture) CountRollupFieldsDeclareEventual(context.Context, string, ...string) (bool, error) {
	return s.eventual, nil
}

func (s *stubDerivedWriteCapture) EnqueueDerivedCountRollupDeltas(_ context.Context, sourceObjectName, sourceRecordID, parentObjectName, parentRecordID string, deltas map[string]int64) error {
	if s.eventSink != nil {
		*s.eventSink = append(*s.eventSink, "enqueue")
	}
	s.events = append(s.events, "enqueue")
	copied := make(map[string]int64, len(deltas))
	for k, v := range deltas {
		copied[k] = v
	}
	s.enqueues = append(s.enqueues, capturedRollupDeltaEnqueue{
		sourceObject: sourceObjectName,
		sourceID:     sourceRecordID,
		parentObject: parentObjectName,
		parentID:     parentRecordID,
		deltas:       copied,
	})
	return s.err
}

func (s *stubDerivedWriteCapture) ApplyDerivedCountRollupDeltas(_ context.Context, objectName, recordID string, deltas map[string]int64, user *models.UserSession) error {
	updates := make(models.SObject, len(deltas))
	for fieldName, delta := range deltas {
		updates[fieldName] = delta
	}
	return s.UpdateDerived(context.Background(), objectName, recordID, updates, user)
}

func (s *stubDerivedWriteCapture) LockDerivedRecord(_ context.Context, objectName, recordID string) error {
	if s.eventSink != nil {
		*s.eventSink = append(*s.eventSink, "lock")
	}
	s.events = append(s.events, "lock")
	s.locks = append(s.locks, capturedDerivedLock{objectName: objectName, recordID: recordID})
	return s.err
}

func (s *stubDerivedWriteCapture) LoadSystemUserSession(_ context.Context) (*models.UserSession, error) {
	if s.loadErr != nil {
		return nil, s.loadErr
	}
	if s.user != nil {
		return s.user, nil
	}
	return testRollupSystemSession(), nil
}

func (s *stubDerivedWriteCapture) GetDerivedRecordForUpdate(_ context.Context, objectName, recordID string) (models.SObject, error) {
	if s.rows == nil {
		return models.SObject{}, nil
	}
	src := s.rows[objectName+"|"+recordID]
	dst := make(models.SObject, len(src))
	for k, v := range src {
		dst[k] = v
	}
	return dst, nil
}

func testRollupSystemSession() *models.UserSession {
	email := constants.SystemUserEmail
	return &models.UserSession{
		ID:    constants.SystemUserID,
		Name:  "Rollup Engine",
		Email: &email,
	}
}

type alwaysPessimisticRollupTxManager struct{}

func (alwaysPessimisticRollupTxManager) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}
func (alwaysPessimisticRollupTxManager) WithPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}
func (alwaysPessimisticRollupTxManager) WithRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return fn(ctx)
}
func (alwaysPessimisticRollupTxManager) WithPessimisticRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return fn(ctx)
}
func (alwaysPessimisticRollupTxManager) IsTransactionActive(context.Context) bool { return true }
func (alwaysPessimisticRollupTxManager) IsPessimisticTx(context.Context) bool     { return true }
func (alwaysPessimisticRollupTxManager) ExtractTx(context.Context) interface{}    { return nil }
func (alwaysPessimisticRollupTxManager) DetachTx(ctx context.Context) context.Context {
	return ctx
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

func newFakeMetadata(schemas map[string]*models.ObjectMetadata) *metadata.MetadataService {
	var list []*models.ObjectMetadata
	for _, v := range schemas {
		list = append(list, v)
	}
	return metadata.NewMetadataService(&stubRollupMetaRepo{schemas: list}, nil, nil)
}

func noopLogger() *logging.Logger { return logging.New(context.Background()) }

// ---------------------------------------------------------------------------
// Test
// ---------------------------------------------------------------------------

// TestProcessRollups_CallsDerivedWriter verifies that ProcessRollups routes
// the rollup write through DerivedWriter.UpdateDerived (not the deleted
// repo.UpdateParentRollup) and passes the correct (object, record, field→value).
func TestProcessRollups_CallsDerivedWriter(t *testing.T) {
	const (
		parentObj   = "Account"
		parentID    = "acc-001"
		childObj    = "Opportunity"
		rollupField = "TotalAmount__c"
		relField    = "AccountId"
	)

	parentSchema := &models.ObjectMetadata{
		APIName: parentObj,
		Fields: []models.FieldMetadata{
			{
				APIName:    rollupField,
				Type:       constants.FieldTypeRollupSummary,
				Updateable: false, // intrinsic-readonly — the whole point of this PR
				RollupConfig: &models.RollupConfig{
					CalcType:          "SUM",
					SummaryObject:     childObj,
					SummaryField:      "Amount",
					RelationshipField: relField,
				},
			},
		},
	}

	meta := newFakeMetadata(map[string]*models.ObjectMetadata{parentObj: parentSchema})
	repo := &stubRollupCalcRepo{calcValue: float64(42)}
	dw := &stubDerivedWriteCapture{}

	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	svc.SetDerivedWriter(dw)

	childRecord := models.SObject{relField: parentID, "Amount": float64(100)}

	if err := svc.ProcessRollups(context.Background(), childObj, childRecord); err != nil {
		t.Fatalf("ProcessRollups() error = %v", err)
	}

	if len(dw.calls) != 1 {
		t.Fatalf("expected 1 UpdateDerived call, got %d", len(dw.calls))
	}
	call := dw.calls[0]
	if call.objectName != parentObj {
		t.Errorf("objectName = %q, want %q", call.objectName, parentObj)
	}
	if call.recordID != parentID {
		t.Errorf("recordID = %q, want %q", call.recordID, parentID)
	}
	if val := call.updates[rollupField]; val != float64(42) {
		t.Errorf("updates[%q] = %v, want 42", rollupField, val)
	}
	if call.user == nil {
		t.Fatal("UpdateDerived user = nil, want rollup system user")
	}
	if call.user.ProfileID != "" {
		t.Errorf("UpdateDerived user ProfileID = %q, want empty non-privileged actor", call.user.ProfileID)
	}
	if call.user.ProfileName != "" {
		t.Errorf("UpdateDerived user ProfileName = %q, want empty internal actor", call.user.ProfileName)
	}
	if call.user.IsSystemAdmin {
		t.Error("UpdateDerived user IsSystemAdmin = true, want false for non-privileged system context actor")
	}
}

func TestProcessRollupsLoadsInternalSystemUserSession(t *testing.T) {
	const (
		parentObj   = "Account"
		parentID    = "acc-001"
		childObj    = "Opportunity"
		rollupField = "TotalAmount__c"
		relField    = "AccountId"
	)

	loadedUser := testRollupSystemSession()
	parentSchema := &models.ObjectMetadata{
		APIName: parentObj,
		Fields: []models.FieldMetadata{{
			APIName:    rollupField,
			Type:       constants.FieldTypeRollupSummary,
			Updateable: false,
			RollupConfig: &models.RollupConfig{
				CalcType:          "SUM",
				SummaryObject:     childObj,
				SummaryField:      "Amount",
				RelationshipField: relField,
			},
		}},
	}

	meta := newFakeMetadata(map[string]*models.ObjectMetadata{parentObj: parentSchema})
	repo := &stubRollupCalcRepo{calcValue: float64(42)}
	dw := &stubDerivedWriteCapture{user: loadedUser}

	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	svc.SetDerivedWriter(dw)

	if err := svc.ProcessRollups(context.Background(), childObj, models.SObject{relField: parentID, "Amount": float64(100)}); err != nil {
		t.Fatalf("ProcessRollups() error = %v", err)
	}
	if len(dw.calls) != 1 {
		t.Fatalf("expected 1 UpdateDerived call, got %d", len(dw.calls))
	}
	user := dw.calls[0].user
	if user != loadedUser {
		t.Fatalf("UpdateDerived user = %#v, want loaded authoritative session", user)
	}

	vars := formula.BuildFormulaUserContext(user)
	profile, ok := vars["__Profile"].(map[string]interface{})
	if !ok {
		t.Fatalf("__Profile = %#v, want formula profile map", vars["__Profile"])
	}
	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.ProfileID != "" {
		t.Fatalf("behavior ProfileID = %q, want empty non-privileged actor", user.ProfileID)
	}
}

func TestProcessRollupsPreservesValidationRuleErrorFromDerivedWrite(t *testing.T) {
	const (
		parentObj   = "Account"
		parentID    = "acc-001"
		childObj    = "Opportunity"
		rollupField = "TotalAmount__c"
		relField    = "AccountId"
	)

	parentSchema := &models.ObjectMetadata{
		APIName: parentObj,
		Fields: []models.FieldMetadata{{
			APIName:    rollupField,
			Type:       constants.FieldTypeRollupSummary,
			Updateable: false,
			RollupConfig: &models.RollupConfig{
				CalcType:          "SUM",
				SummaryObject:     childObj,
				SummaryField:      "Amount",
				RelationshipField: relField,
			},
		}},
	}

	meta := newFakeMetadata(map[string]*models.ObjectMetadata{parentObj: parentSchema})
	repo := &stubRollupCalcRepo{calcValue: float64(42)}
	ruleErr := appErrors.NewValidationRuleError("Rollup_Profile_Gate", "Description is required")
	dw := &stubDerivedWriteCapture{err: ruleErr}

	svc := NewRollupService(repo, meta, alwaysPessimisticRollupTxManager{}, noopLogger())
	svc.SetDerivedWriter(dw)

	err := svc.ProcessRollups(context.Background(), childObj, models.SObject{relField: parentID, "Amount": float64(100)})
	if err == nil {
		t.Fatal("ProcessRollups = nil, want validation rule error")
	}
	var gotRule *appErrors.ValidationRuleError
	if !errors.As(err, &gotRule) {
		t.Fatalf("ProcessRollups error = %T (%v), want ValidationRuleError", err, err)
	}
	if gotRule != ruleErr {
		t.Fatalf("ProcessRollups returned %#v, want original rule error", gotRule)
	}
	if appErrors.IsInternal(err) {
		t.Fatalf("ProcessRollups error = %v, want non-internal validation classification", err)
	}
}
