package records

import (
	"context"
	"strings"
	"testing"
	"time"

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

func TestRollupDeltaDedupeIdentityIncludesCompleteRuntimeMetadataBinding(t *testing.T) {
	base := runtimeRollupBinding()
	first, err := rollupDeltaDedupeKey("Story__c", "story-001", "Epic__c", "epic-001", "StoryCount__c", &base)
	if err != nil {
		t.Fatal(err)
	}
	repeated, err := rollupDeltaDedupeKey("Story__c", "story-001", "Epic__c", "epic-001", "StoryCount__c", &base)
	if err != nil {
		t.Fatal(err)
	}
	newRevision := base
	newRevision.CatalogRevision++
	second, err := rollupDeltaDedupeKey("Story__c", "story-001", "Epic__c", "epic-001", "StoryCount__c", &newRevision)
	if err != nil {
		t.Fatal(err)
	}
	if first != repeated || first == second {
		t.Fatalf("dedupe keys first=%q repeated=%q new-revision=%q", first, repeated, second)
	}

	// The legacy in-process queue remains idempotent without pretending to own
	// a Runtime definition binding. Its identity is deliberately separate from
	// independently produced revision-bound work during the ownership cutover.
	legacy, err := rollupDeltaDedupeKey("Story__c", "story-001", "Epic__c", "epic-001", "StoryCount__c", nil)
	if err != nil {
		t.Fatal(err)
	}
	if legacy == first {
		t.Fatal("legacy and revision-bound rollup work share a dedupe identity")
	}
}

func TestApplyRollupDeltaBacklogStatsPublishesBoundedLabels(t *testing.T) {
	rollupDeltaQueueDepth.Reset()
	rollupDeltaHotParentLaneDepth.Reset()

	applyRollupDeltaBacklogStats(rollupDeltaBacklogStats{
		depthByState: map[string]int64{
			rollupDeltaStatusPending:   7,
			rollupDeltaStatusProcessed: 11,
		},
		pendingParentLanes:      3,
		pendingRows:             7,
		hotParentLaneDepthRanks: []int64{5, 2},
		oldestPendingAgeSeconds: 91,
		retryRows:               4,
		maxRetryCount:           2,
	})

	if got := testutil.ToFloat64(rollupDeltaQueueDepth.WithLabelValues(rollupDeltaStatusPending)); got != 7 {
		t.Fatalf("pending queue depth metric = %v, want 7", got)
	}
	if got := testutil.ToFloat64(rollupDeltaPendingParentLanes); got != 3 {
		t.Fatalf("pending parent lanes metric = %v, want 3", got)
	}
	if got := testutil.ToFloat64(rollupDeltaPendingRows); got != 7 {
		t.Fatalf("pending rows metric = %v, want 7", got)
	}
	if got := testutil.ToFloat64(rollupDeltaHotParentLaneDepth.WithLabelValues("1")); got != 5 {
		t.Fatalf("hot parent lane rank 1 depth metric = %v, want 5", got)
	}
	if got := testutil.ToFloat64(rollupDeltaHotParentLaneDepth.WithLabelValues("2")); got != 2 {
		t.Fatalf("hot parent lane rank 2 depth metric = %v, want 2", got)
	}
	if got := testutil.ToFloat64(rollupDeltaHotParentLaneDepth.WithLabelValues("3")); got != 0 {
		t.Fatalf("hot parent lane rank 3 depth metric = %v, want 0", got)
	}
	if got := testutil.ToFloat64(rollupDeltaOldestPendingAgeSeconds); got != 91 {
		t.Fatalf("oldest pending age metric = %v, want 91", got)
	}
	if got := testutil.ToFloat64(rollupDeltaRetryRows); got != 4 {
		t.Fatalf("retry rows metric = %v, want 4", got)
	}
	if got := testutil.ToFloat64(rollupDeltaMaxRetryCount); got != 2 {
		t.Fatalf("max retry count metric = %v, want 2", got)
	}
}

func TestInitializeRollupDeltaBacklogMetricsPublishesFixedSeries(t *testing.T) {
	applyRollupDeltaBacklogStats(rollupDeltaBacklogStats{
		depthByState:            map[string]int64{rollupDeltaStatusPending: 8},
		pendingParentLanes:      3,
		pendingRows:             8,
		hotParentLaneDepthRanks: []int64{5},
		oldestPendingAgeSeconds: 72,
		retryRows:               2,
		maxRetryCount:           4,
	})
	InitializeRollupDeltaBacklogMetrics()

	for _, state := range rollupDeltaBacklogStates {
		if got := testutil.ToFloat64(rollupDeltaQueueDepth.WithLabelValues(state)); got != 0 {
			t.Fatalf("queue depth{%s} = %v after initialization, want 0", state, got)
		}
	}
	for _, rank := range rollupDeltaHotParentLaneRanks {
		if got := testutil.ToFloat64(rollupDeltaHotParentLaneDepth.WithLabelValues(rank)); got != 0 {
			t.Fatalf("hot parent lane depth{%s} = %v after initialization, want 0", rank, got)
		}
	}
	for name, gauge := range map[string]prometheus.Gauge{
		"oldest pending age":   rollupDeltaOldestPendingAgeSeconds,
		"pending parent lanes": rollupDeltaPendingParentLanes,
		"pending rows":         rollupDeltaPendingRows,
		"retry rows":           rollupDeltaRetryRows,
		"max retry count":      rollupDeltaMaxRetryCount,
	} {
		if got := testutil.ToFloat64(gauge); got != 0 {
			t.Fatalf("%s = %v after initialization, want 0", name, got)
		}
	}
}

func TestRollupDeltaWorkerUsesBoundedTickContext(t *testing.T) {
	src := readRecordsSourceForRawAccessStructure(t, "rollup_delta_queue.go")

	if strings.Contains(src, "context.WithCancel(context.Background())") {
		t.Fatal("rollup delta worker must not drain with an unbounded context.Background()")
	}
	for _, symbol := range []string{
		"rollupDeltaWorkerTickTimeout = 5 * time.Minute",
		"func rollupDeltaWorkerTickContext() (context.Context, context.CancelFunc)",
		"context.WithTimeout(context.Background(), rollupDeltaWorkerTickTimeout)",
		"tickCtx, cancel := rollupDeltaWorkerTickContext()",
	} {
		if !strings.Contains(src, symbol) {
			t.Fatalf("rollup_delta_queue.go is missing bounded worker context fragment %q", symbol)
		}
	}
}

func TestRollupDeltaWorkerTickContextIsBounded(t *testing.T) {
	ctx, cancel := rollupDeltaWorkerTickContext()
	defer cancel()

	deadline, ok := ctx.Deadline()
	if !ok {
		t.Fatal("rollup delta worker tick context has no deadline")
	}
	if time.Until(deadline) <= 0 {
		t.Fatalf("rollup delta worker tick deadline is already expired: %v", deadline)
	}
}

func TestApplyDerivedCountRollupDeltasRejectsMetadataOwnedUpdateHooks(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*models.ObjectMetadata)
	}{
		{
			name: "write validator",
			mutate: func(schema *models.ObjectMetadata) {
				schema.AccessPolicy = &models.ObjectAccessPolicy{
					WriteValidation: &models.RecordWriteValidationAccessPolicy{
						Validators: []models.RecordWriteValidationPolicy{{
							Phase:      string(recordWriteValidationBeforeUpdate),
							Capability: "test.write.validate",
						}},
					},
				}
			},
		},
		{
			name: "write canonicalizer",
			mutate: func(schema *models.ObjectMetadata) {
				schema.AccessPolicy = &models.ObjectAccessPolicy{
					WriteValidation: &models.RecordWriteValidationAccessPolicy{
						Canonicalizers: []models.RecordWriteCanonicalizationPolicy{{
							Phase:      string(recordWriteValidationBeforeUpdate),
							Capability: "test.write.canonicalize",
						}},
					},
				}
			},
		},
		{
			name: "required validation rule",
			mutate: func(schema *models.ObjectMetadata) {
				schema.AccessPolicy = &models.ObjectAccessPolicy{
					WriteValidation: &models.RecordWriteValidationAccessPolicy{
						RequiredValidationRules: []models.RecordWriteValidationRuleRequirement{{
							Phase:     string(recordWriteValidationBeforeUpdate),
							Name:      "test_required_rule",
							OwnerPack: "test-pack",
						}},
					},
				}
			},
		},
		{
			name: "cache invalidation",
			mutate: func(schema *models.ObjectMetadata) {
				schema.AccessPolicy = &models.ObjectAccessPolicy{
					CacheInvalidation: &models.RecordCacheInvalidationAccessPolicy{
						Handlers: []models.RecordCacheInvalidationPolicy{{
							Capability: "test.cache.invalidate",
						}},
					},
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			schema := rollupDeltaQueueCountSchema()
			tt.mutate(schema)
			repo := &rollupSummaryRecordRepo{old: models.SObject{constants.FieldID: "epic-001"}}
			ps := newRollupSummaryTestService(schema, repo)

			err := ps.ApplyDerivedCountRollupDeltas(context.Background(), "Epic__c", "epic-001", map[string]int64{
				"StoryCount__c": 1,
			}, &models.UserSession{ID: constants.SystemUserID})
			if err == nil {
				t.Fatal("ApplyDerivedCountRollupDeltas succeeded, want unsafe metadata-owned update hook rejection")
			}
			if repo.updateCalled {
				t.Fatal("ApplyDerivedCountRollupDeltas reached raw update path for unsafe metadata-owned update hook")
			}
		})
	}
}

func rollupDeltaQueueCountSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Epic__c",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldLastModifiedByID, Type: constants.FieldTypeReference, Createable: false, Updateable: false},
			{
				APIName:    "StoryCount__c",
				Type:       constants.FieldTypeRollupSummary,
				Createable: false,
				Updateable: false,
				RollupConfig: &models.RollupConfig{
					CalcType:          "COUNT",
					SummaryObject:     "Story__c",
					SummaryField:      "*",
					RelationshipField: "Epic__c",
				},
			},
		},
	}
	makeFieldsMap(schema)
	return schema
}
