package analytics

import (
	"context"
	"fmt"
	"sort"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/prometheus/client_golang/prometheus/testutil"
)

type recalcTxActiveKey struct{}
type recalcTxPessimisticKey struct{}

type recordingRecalculateTxManager struct {
	detachCalls      int
	optimisticCalls  int
	pessimisticCalls int
	retryCalls       int
}

// buildPage mirrors the repository's keyset semantics: WHERE Id > afterID
// ORDER BY Id ASC LIMIT n. It must not fall back to "start from the beginning"
// for a cursor that names no existing row — production returns the rows sorting
// above it, and a fixture that restarts instead would hide exactly the
// resume bugs these tests exist to catch.
func buildPage(ids []string, afterID string, limit int) []string {
	sorted := append([]string(nil), ids...)
	sort.Strings(sorted)
	start := 0
	for start < len(sorted) && sorted[start] <= afterID {
		start++
	}
	if afterID == "" {
		start = 0
	}
	if start >= len(sorted) {
		return nil
	}
	end := start + limit
	if end > len(sorted) {
		end = len(sorted)
	}
	return append([]string(nil), sorted[start:end]...)
}

func (m *recordingRecalculateTxManager) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	m.optimisticCalls++
	txCtx := context.WithValue(ctx, recalcTxActiveKey{}, true)
	txCtx = context.WithValue(txCtx, recalcTxPessimisticKey{}, false)
	return fn(txCtx)
}

func (m *recordingRecalculateTxManager) WithPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	if m.IsTransactionActive(ctx) && !m.IsPessimisticTx(ctx) {
		return fmt.Errorf("WithPessimisticTransaction called inside a non-pessimistic transaction")
	}
	m.pessimisticCalls++
	txCtx := context.WithValue(ctx, recalcTxActiveKey{}, true)
	txCtx = context.WithValue(txCtx, recalcTxPessimisticKey{}, true)
	return fn(txCtx)
}

func (m *recordingRecalculateTxManager) WithRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return m.WithTransaction(ctx, fn)
}

func (m *recordingRecalculateTxManager) WithPessimisticRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	m.retryCalls++
	return m.WithPessimisticTransaction(ctx, fn)
}

func (m *recordingRecalculateTxManager) IsTransactionActive(ctx context.Context) bool {
	active, _ := ctx.Value(recalcTxActiveKey{}).(bool)
	return active
}

func (m *recordingRecalculateTxManager) IsPessimisticTx(ctx context.Context) bool {
	pessimistic, _ := ctx.Value(recalcTxPessimisticKey{}).(bool)
	return pessimistic
}

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

func (m *recordingRecalculateTxManager) DetachTx(ctx context.Context) context.Context {
	m.detachCalls++
	ctx = context.WithValue(ctx, recalcTxActiveKey{}, false)
	ctx = context.WithValue(ctx, recalcTxPessimisticKey{}, false)
	return ctx
}

var _ ports.TransactionManager = (*recordingRecalculateTxManager)(nil)

type recalculateDerivedWriter struct {
	*stubDerivedWriteCapture
	txManager *recordingRecalculateTxManager
}

func (w *recalculateDerivedWriter) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return w.txManager.WithPessimisticRetry(ctx, fn, 3)
}

func assertRecalculateSkippedLockMetricUnchanged(t *testing.T, before float64) {
	t.Helper()

	after := testutil.ToFloat64(rollupLockSkippedOnOptimistic)
	if after != before {
		t.Fatalf("rollupLockSkippedOnOptimistic = %v after happy-path call, want unchanged at %v", after, before)
	}
}

func assertEventSequence(t *testing.T, got []string, want []string) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("events = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("events[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
		}
	}
}

func TestRecalculateAllDetachesInheritedTransactionPerParent(t *testing.T) {
	const (
		parentObj   = "Project"
		childObj    = "Task"
		rollupField = "TotalBudget"
		relField    = "ProjectId"
	)

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

	before := testutil.ToFloat64(rollupLockSkippedOnOptimistic)
	var events []string
	meta := newFakeMetadata(map[string]*models.ObjectMetadata{
		parentObj: parentSchema,
		childObj: {
			APIName: childObj,
			Fields: []models.FieldMetadata{
				{APIName: relField, Type: constants.FieldTypeReference},
				{APIName: "Budget", Type: constants.FieldTypeCurrency},
			},
		},
	})
	repo := &stubRollupCalcRepo{
		calcValue: float64(42),
		eventSink: &events,
		parentIDs: []string{"proj-002", "proj-001"},
	}
	txm := &recordingRecalculateTxManager{}
	dw := &recalculateDerivedWriter{
		stubDerivedWriteCapture: &stubDerivedWriteCapture{eventSink: &events},
		txManager:               txm,
	}
	svc := NewRollupService(repo, meta, txm, noopLogger())
	svc.SetDerivedWriter(dw)

	outerCtx := context.WithValue(context.Background(), recalcTxActiveKey{}, true)
	outerCtx = context.WithValue(outerCtx, recalcTxPessimisticKey{}, false)

	progress, err := svc.RecalculateAll(outerCtx, parentObj, ports.RollupRecalculateBounds{})
	if err != nil {
		t.Fatalf("RecalculateAll: %v", err)
	}
	if progress.Recalculated != 2 {
		t.Fatalf("RecalculateAll count = %d, want 2", progress.Recalculated)
	}
	if txm.detachCalls != 2 {
		t.Fatalf("DetachTx calls = %d, want 2", txm.detachCalls)
	}
	if txm.optimisticCalls != 0 {
		t.Fatalf("WithTransaction calls = %d, want 0", txm.optimisticCalls)
	}
	if txm.pessimisticCalls != 2 {
		t.Fatalf("WithPessimisticTransaction calls = %d, want 2", txm.pessimisticCalls)
	}
	if txm.retryCalls != 2 {
		t.Fatalf("WithPessimisticRetry calls = %d, want 2", txm.retryCalls)
	}
	if repo.currentReadCalls != 2 {
		t.Fatalf("CalculateRollupCurrentRead calls = %d, want 2", repo.currentReadCalls)
	}
	if repo.calculateCalls != 0 {
		t.Fatalf("CalculateRollup calls = %d, want 0", repo.calculateCalls)
	}
	if len(dw.calls) != 2 {
		t.Fatalf("UpdateDerived calls = %d, want 2", len(dw.calls))
	}
	if len(dw.locks) != 2 {
		t.Fatalf("LockDerivedRecord calls = %d, want 2", len(dw.locks))
	}
	assertEventSequence(t, events, []string{"lock", "calc", "update", "lock", "calc", "update"})
	assertRecalculateSkippedLockMetricUnchanged(t, before)
}

func TestRecalculateAllPagesThroughLargeParentSets(t *testing.T) {
	const (
		parentObj = "Project"
		childObj  = "Task"
		relField  = "ProjectId"
	)

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

	var ids []string
	for i := 0; i < rollupRecalculatePageSize+1; i++ {
		ids = append(ids, fmt.Sprintf("proj-%06d", i+1))
	}

	meta := newFakeMetadata(map[string]*models.ObjectMetadata{
		parentObj: parentSchema,
		childObj: {
			APIName: childObj,
			Fields: []models.FieldMetadata{
				{APIName: relField, Type: constants.FieldTypeReference},
				{APIName: "Budget", Type: constants.FieldTypeCurrency},
			},
		},
	})
	repo := &stubRollupCalcRepo{
		calcValue: float64(42),
		parentIDs: ids,
	}
	txm := &recordingRecalculateTxManager{}
	dw := &recalculateDerivedWriter{
		stubDerivedWriteCapture: &stubDerivedWriteCapture{},
		txManager:               txm,
	}
	svc := NewRollupService(repo, meta, txm, noopLogger())
	svc.SetDerivedWriter(dw)

	progress, err := svc.RecalculateAll(context.Background(), parentObj, ports.RollupRecalculateBounds{})
	if err != nil {
		t.Fatalf("RecalculateAll: %v", err)
	}
	if progress.Recalculated != len(ids) {
		t.Fatalf("RecalculateAll count = %d, want %d", progress.Recalculated, len(ids))
	}
	if repo.currentReadCalls != len(ids) {
		t.Fatalf("CalculateRollupCurrentRead calls = %d, want %d", repo.currentReadCalls, len(ids))
	}
	if repo.calculateCalls != 0 {
		t.Fatalf("CalculateRollup calls = %d, want 0", repo.calculateCalls)
	}
	if len(dw.calls) != len(ids) {
		t.Fatalf("UpdateDerived calls = %d, want %d", len(dw.calls), len(ids))
	}
	if len(dw.locks) != len(ids) {
		t.Fatalf("LockDerivedRecord calls = %d, want %d", len(dw.locks), len(ids))
	}
}

func TestRecalculateAllProcessesEachRollupFieldSequentially(t *testing.T) {
	const (
		parentObj = "Project"
		childObj  = "Task"
		relField  = "ProjectId"
		parentID  = "proj-001"
	)

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

	before := testutil.ToFloat64(rollupLockSkippedOnOptimistic)
	var events []string
	meta := newFakeMetadata(map[string]*models.ObjectMetadata{
		parentObj: parentSchema,
		childObj: {
			APIName: childObj,
			Fields: []models.FieldMetadata{
				{APIName: relField, Type: constants.FieldTypeReference},
				{APIName: "Budget", Type: constants.FieldTypeCurrency},
				{APIName: "Id", Type: constants.FieldTypeString},
			},
		},
	})
	repo := &stubRollupCalcRepo{
		calcValue: float64(42),
		eventSink: &events,
		parentIDs: []string{parentID},
	}
	txm := &recordingRecalculateTxManager{}
	dw := &recalculateDerivedWriter{
		stubDerivedWriteCapture: &stubDerivedWriteCapture{eventSink: &events},
		txManager:               txm,
	}
	svc := NewRollupService(repo, meta, txm, noopLogger())
	svc.SetDerivedWriter(dw)

	progress, err := svc.RecalculateAll(context.Background(), parentObj, ports.RollupRecalculateBounds{})
	if err != nil {
		t.Fatalf("RecalculateAll: %v", err)
	}
	if progress.Recalculated != 1 {
		t.Fatalf("RecalculateAll count = %d, want 1", progress.Recalculated)
	}
	if txm.detachCalls != 1 {
		t.Fatalf("DetachTx calls = %d, want 1", txm.detachCalls)
	}
	if txm.optimisticCalls != 0 {
		t.Fatalf("WithTransaction calls = %d, want 0", txm.optimisticCalls)
	}
	if txm.pessimisticCalls != 1 {
		t.Fatalf("WithPessimisticTransaction calls = %d, want 1", txm.pessimisticCalls)
	}
	if txm.retryCalls != 1 {
		t.Fatalf("WithPessimisticRetry calls = %d, want 1", txm.retryCalls)
	}
	if repo.currentReadCalls != 2 {
		t.Fatalf("CalculateRollupCurrentRead calls = %d, want 2", repo.currentReadCalls)
	}
	if repo.calculateCalls != 0 {
		t.Fatalf("CalculateRollup calls = %d, want 0", repo.calculateCalls)
	}
	if len(dw.locks) != 1 {
		t.Fatalf("LockDerivedRecord calls = %d, want 1", len(dw.locks))
	}
	if len(dw.calls) != 1 {
		t.Fatalf("UpdateDerived calls = %d, want 1 merged parent update", len(dw.calls))
	}
	if updates := dw.calls[0].updates; len(updates) != 2 || updates["TotalBudget"] == nil || updates["TaskCount"] == nil {
		t.Fatalf("merged updates = %#v, want TotalBudget + TaskCount", updates)
	}
	assertEventSequence(t, events, []string{"lock", "calc", "calc", "update"})
	assertRecalculateSkippedLockMetricUnchanged(t, before)
}

func TestRecalculateAllReturnsErrorWhenParentCalculationFails(t *testing.T) {
	const (
		parentObj   = "Project"
		childObj    = "Task"
		rollupField = "TotalBudget"
		relField    = "ProjectId"
	)

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

	var events []string
	meta := newFakeMetadata(map[string]*models.ObjectMetadata{
		parentObj: parentSchema,
		childObj: {
			APIName: childObj,
			Fields: []models.FieldMetadata{
				{APIName: relField, Type: constants.FieldTypeReference},
				{APIName: "Budget", Type: constants.FieldTypeCurrency},
			},
		},
	})
	repo := &stubRollupCalcRepo{
		calcValue:            float64(42),
		currentReadErrAtCall: 2,
		currentReadErr:       fmt.Errorf("calculate failed"),
		eventSink:            &events,
		parentIDs:            []string{"proj-001", "proj-002"},
	}
	txm := &recordingRecalculateTxManager{}
	dw := &recalculateDerivedWriter{
		stubDerivedWriteCapture: &stubDerivedWriteCapture{eventSink: &events},
		txManager:               txm,
	}
	svc := NewRollupService(repo, meta, txm, noopLogger())
	svc.SetDerivedWriter(dw)

	progress, err := svc.RecalculateAll(context.Background(), parentObj, ports.RollupRecalculateBounds{})
	if err == nil {
		t.Fatal("RecalculateAll() error = nil, want failure on second parent")
	}
	if progress.Recalculated != 1 {
		t.Fatalf("RecalculateAll count = %d, want 1 successful parent before failure", progress.Recalculated)
	}
	if txm.detachCalls != 2 {
		t.Fatalf("DetachTx calls = %d, want 2", txm.detachCalls)
	}
	if txm.optimisticCalls != 0 {
		t.Fatalf("WithTransaction calls = %d, want 0", txm.optimisticCalls)
	}
	if txm.pessimisticCalls != 2 {
		t.Fatalf("WithPessimisticTransaction calls = %d, want 2", txm.pessimisticCalls)
	}
	if txm.retryCalls != 2 {
		t.Fatalf("WithPessimisticRetry calls = %d, want 2", txm.retryCalls)
	}
	if repo.currentReadCalls != 2 {
		t.Fatalf("CalculateRollupCurrentRead calls = %d, want 2 before failure", repo.currentReadCalls)
	}
	if repo.calculateCalls != 0 {
		t.Fatalf("CalculateRollup calls = %d, want 0 before failure", repo.calculateCalls)
	}
	if len(dw.locks) != 2 {
		t.Fatalf("LockDerivedRecord calls = %d, want 2 before failure", len(dw.locks))
	}
	if len(dw.calls) != 1 {
		t.Fatalf("UpdateDerived calls = %d, want 1 before failure", len(dw.calls))
	}
	assertEventSequence(t, events, []string{"lock", "calc", "update", "lock", "calc"})
}

func TestRecalculateAllUsesCurrentReadForViewRoutedChildSource(t *testing.T) {
	const (
		parentObj   = "Opportunity"
		childObj    = "OpportunityLineItem"
		rollupField = "Amount"
		relField    = "OpportunityId"
		parentID    = "006-parent"
	)

	activeMode := "active"
	formula := "Quantity * UnitPrice"
	returnType := constants.FieldTypeCurrency

	parentSchema := &models.ObjectMetadata{
		APIName: parentObj,
		Fields: []models.FieldMetadata{{
			APIName:    rollupField,
			Type:       constants.FieldTypeRollupSummary,
			Updateable: false,
			RollupConfig: &models.RollupConfig{
				CalcType:          "SUM",
				SummaryObject:     childObj,
				SummaryField:      "TotalPrice",
				RelationshipField: relField,
			},
		}},
	}
	childSchema := &models.ObjectMetadata{
		APIName: childObj,
		Fields: []models.FieldMetadata{
			{APIName: relField, Type: constants.FieldTypeReference},
			{
				APIName:     "TotalPrice",
				Type:        constants.FieldTypeCurrency,
				Formula:     &formula,
				FormulaMode: &activeMode,
				ReturnType:  &returnType,
			},
		},
	}

	before := testutil.ToFloat64(rollupLockSkippedOnOptimistic)
	var events []string
	meta := newFakeMetadata(map[string]*models.ObjectMetadata{
		parentObj: parentSchema,
		childObj:  childSchema,
	})
	repo := &stubRollupCalcRepo{
		calcValue: float64(42),
		eventSink: &events,
		parentIDs: []string{parentID},
	}
	txm := &recordingRecalculateTxManager{}
	dw := &recalculateDerivedWriter{
		stubDerivedWriteCapture: &stubDerivedWriteCapture{eventSink: &events},
		txManager:               txm,
	}
	svc := NewRollupService(repo, meta, txm, noopLogger())
	svc.SetDerivedWriter(dw)

	progress, err := svc.RecalculateAll(context.Background(), parentObj, ports.RollupRecalculateBounds{})
	if err != nil {
		t.Fatalf("RecalculateAll: %v", err)
	}
	if progress.Recalculated != 1 {
		t.Fatalf("RecalculateAll count = %d, want 1 with current-read path", progress.Recalculated)
	}
	if txm.optimisticCalls != 0 {
		t.Fatalf("WithTransaction calls = %d, want 0 for current-read path", txm.optimisticCalls)
	}
	if txm.pessimisticCalls != 1 {
		t.Fatalf("WithPessimisticTransaction calls = %d, want 1 for current-read path", txm.pessimisticCalls)
	}
	if txm.retryCalls != 1 {
		t.Fatalf("WithPessimisticRetry calls = %d, want 1 for current-read path", txm.retryCalls)
	}
	if repo.currentReadCalls != 1 {
		t.Fatalf("CalculateRollupCurrentRead calls = %d, want 1 for view-routed current-read path", repo.currentReadCalls)
	}
	if repo.calculateCalls != 0 {
		t.Fatalf("CalculateRollup calls = %d, want 0 for view-routed current-read path", repo.calculateCalls)
	}
	if len(dw.calls) != 1 {
		t.Fatalf("UpdateDerived calls = %d, want 1 on current-read path", len(dw.calls))
	}
	if len(dw.locks) != 1 {
		t.Fatalf("LockDerivedRecord calls = %d, want 1 on current-read path", len(dw.locks))
	}
	assertEventSequence(t, events, []string{"lock", "calc", "update"})
	assertRecalculateSkippedLockMetricUnchanged(t, before)
}

func TestRecalculateAllRejectsUnsafeRollupFilterBeforeRepositoryCall(t *testing.T) {
	const (
		parentObj   = "Project"
		childObj    = "Task"
		rollupField = "TotalBudget"
		relField    = "ProjectId"
		parentID    = "proj-001"
	)

	filter := "Amount > 0; DROP TABLE Task"
	parentSchema := &models.ObjectMetadata{
		APIName: parentObj,
		Fields: []models.FieldMetadata{{
			APIName:    rollupField,
			Type:       constants.FieldTypeRollupSummary,
			Updateable: false,
			RollupConfig: &models.RollupConfig{
				CalcType:          "SUM",
				SummaryObject:     childObj,
				SummaryField:      "Budget",
				RelationshipField: relField,
				Filter:            &filter,
			},
		}},
	}

	meta := newFakeMetadata(map[string]*models.ObjectMetadata{
		parentObj: parentSchema,
		childObj: {
			APIName: childObj,
			Fields: []models.FieldMetadata{
				{APIName: relField, Type: constants.FieldTypeReference},
				{APIName: "Budget", Type: constants.FieldTypeCurrency},
			},
		},
	})
	repo := &stubRollupCalcRepo{
		calcValue: float64(42),
		parentIDs: []string{parentID},
	}
	txm := &recordingRecalculateTxManager{}
	dw := &recalculateDerivedWriter{
		stubDerivedWriteCapture: &stubDerivedWriteCapture{},
		txManager:               txm,
	}
	svc := NewRollupService(repo, meta, txm, noopLogger())
	svc.SetDerivedWriter(dw)

	progress, err := svc.RecalculateAll(context.Background(), parentObj, ports.RollupRecalculateBounds{})
	if err == nil {
		t.Fatal("RecalculateAll() error = nil, want unsafe filter rejection")
	}
	if progress.Recalculated != 0 {
		t.Fatalf("RecalculateAll count = %d, want 0 when filter is rejected", progress.Recalculated)
	}
	if repo.currentReadCalls != 0 {
		t.Fatalf("CalculateRollupCurrentRead calls = %d, want 0 when filter is rejected pre-repository", repo.currentReadCalls)
	}
	if repo.calculateCalls != 0 {
		t.Fatalf("CalculateRollup calls = %d, want 0 when filter is rejected pre-repository", repo.calculateCalls)
	}
	if len(dw.locks) != 1 {
		t.Fatalf("LockDerivedRecord calls = %d, want 1 before filter validation", len(dw.locks))
	}
	if len(dw.calls) != 0 {
		t.Fatalf("UpdateDerived calls = %d, want 0 when filter is rejected", len(dw.calls))
	}
}
