package records

// T2b: rollup-held interleave test
//
// Design §5 T2b: "high-water acquired by a REAL rollup derived-write; preview
// discovers 5 late lookup keys | mixed rollup+lookup keys merge into forced
// prelocks; converges | FAILS pre-fix".
//
// This file uses the persistence_rollup_parent_lock_retry_test.go:217 harness
// (newRollupRetryService, rollupRetryRecordRepo) as the pattern.

import (
	"context"
	"testing"

	analyticsFeature "github.com/nexuscrm/analytics"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// t2bLeafSchema is a Leaf__c schema with five FK fields (to Alpha__c .. Epsilon__c)
// in addition to the MidId__c rollup-source FK.  All five target objects
// (Alpha, Beta, Gamma, Delta, Epsilon) sort alphabetically before Zenith__c,
// which is the rollup parent that the rollup derived-write will acquire as the
// high-water mark during the test.
func t2bLeafSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Leaf__c",
		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: constants.FieldIsDeleted, Type: constants.FieldTypeBoolean, Createable: false, Updateable: false},
			{APIName: "Amount__c", Type: constants.FieldTypeDouble, Createable: true, Updateable: true},
			{APIName: "MidId__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Mid__c"}, Createable: true, Updateable: true},
			lockRequiredLookupField("AlphaId__c", "Alpha__c"),
			lockRequiredLookupField("BetaId__c", "Beta__c"),
			lockRequiredLookupField("GammaId__c", "Gamma__c"),
			lockRequiredLookupField("DeltaId__c", "Delta__c"),
			lockRequiredLookupField("EpsilonId__c", "Epsilon__c"),
		},
	}
	makeFieldsMap(schema)
	return schema
}

// t2bMidSchema is a Mid__c schema with a rollup summary of Leaf.Amount__c and a
// ZenithId__c FK that points to Zenith__c.  When the rollup derived-write
// updates Mid (via ps.Update), prelockInitialUpdateParents acquires
// Zenith__c/z-1 as the pessimistic high-water mark.
func t2bMidSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "Mid__c",
		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},
			lockRequiredLookupField("ZenithId__c", "Zenith__c"),
			{APIName: "LeafTotal__c", Type: constants.FieldTypeRollupSummary, Createable: false, Updateable: false, RollupConfig: &models.RollupConfig{
				CalcType:          "SUM",
				SummaryObject:     "Leaf__c",
				SummaryField:      "Amount__c",
				RelationshipField: "MidId__c",
			}},
		},
	}
	makeFieldsMap(schema)
	return schema
}

func toyDeltaSchema() *models.ObjectMetadata {
	s := &models.ObjectMetadata{
		APIName:      "Delta__c",
		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},
		},
	}
	makeFieldsMap(s)
	return s
}

func toyEpsilonSchema() *models.ObjectMetadata {
	s := &models.ObjectMetadata{
		APIName:      "Epsilon__c",
		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},
		},
	}
	makeFieldsMap(s)
	return s
}

// newT2bService builds a PersistenceService+RollupService for T2b.
// The rollup config mirrors newRollupRetryService but uses t2bLeafSchema and
// t2bMidSchema (Mid has ZenithId__c pre-set so the derived Update acquires the
// Zenith__c parent as the high-water mark without requiring a before-save event
// mutation).
func newT2bService(repo *rollupRetryRecordRepo, txm *recordingBulkTxManager) (*PersistenceService, *analyticsFeature.RollupService) {
	return newRollupRetryService(repo, txm,
		t2bLeafSchema(),
		t2bMidSchema(),
		toyZenithSchema(),
		toyAlphaSchema(),
		toyBetaSchema(),
		toyGammaSchema(),
		toyDeltaSchema(),
		toyEpsilonSchema(),
	)
}

// TestPreviewBeforeUpdateRollupLookupInterleave is the T2b task-mandated test.
//
// Scenario:
//   - rollupSvc.ProcessRollups writes a derived update to Mid__c/mid-001 which
//     has ZenithId__c="z-1" pre-set, so prelockInitialUpdateParents acquires
//     Zenith__c/z-1 as the pessimistic high-water mark.
//   - In the SAME coordinator scope (same pessimistic attempt), ps.PreviewBeforeUpdate
//     is called on Leaf__c/leaf-001 with five FK fields (AlphaId__c..EpsilonId__c)
//     whose parent objects all sort alphabetically before Zenith__c.
//   - validateNonPolymorphicLookups calls lockLookupParentsIfCoordinated with all 5
//     targets → LockParentRows returns outOfOrderParentLockRetryError{keys:[Alpha..Epsilon]}.
//   - RunInPessimisticTransaction catches the error, merges [Alpha..Epsilon] into
//     forcedPrelocks, and retries the ENTIRE fn with those 5 keys pre-acquired.
//
// Attempt 2:
//   - Force-prelock [Alpha,Beta,Delta,Epsilon,Gamma] (sorted).
//   - ProcessRollups acquires Zenith__c/z-1 (after Alpha..Epsilon, in order) → success.
//   - PreviewBeforeUpdate: lockLookupParentsIfCoordinated → LockParentRows([Alpha..Epsilon])
//     → all already held → nil → existence check passes → success.
//   - Transaction commits; total pessimisticCalls == 2.
//
// Pre-fix (per-key loop): attempt 1 returns only the first out-of-order key (Alpha)
// → retry carries only Alpha → Zenith still sorts after Alpha → PreviewBeforeUpdate
// still fails on Beta → total retries exceed budget → FAILS.
func TestPreviewBeforeUpdateRollupLookupInterleave(t *testing.T) {
	repo := &rollupRetryRecordRepo{
		rows: map[string]map[string]models.SObject{
			// Leaf record — NO FK values set, so PreviewBeforeUpdate sees effective changes.
			"Leaf__c": {
				"leaf-001": {
					constants.FieldID:   "leaf-001",
					constants.FieldName: "leaf-001",
					"Amount__c":         float64(0),
					"MidId__c":          "mid-001",
				},
			},
			// Mid record has ZenithId__c pre-set so the rollup Update step acquires
			// Zenith__c/z-1 via prelockInitialUpdateParents (no before-save mutation needed).
			"Mid__c": {
				"mid-001": {
					constants.FieldID:   "mid-001",
					constants.FieldName: "mid-001",
					"ZenithId__c":       "z-1",
					"LeafTotal__c":      float64(0),
				},
			},
			"Zenith__c": {
				"z-1": {constants.FieldID: "z-1"},
			},
			// All five FK parents exist so the post-lock existence check passes.
			"Alpha__c":   {"a-1": {constants.FieldID: "a-1"}},
			"Beta__c":    {"b-1": {constants.FieldID: "b-1"}},
			"Gamma__c":   {"g-1": {constants.FieldID: "g-1"}},
			"Delta__c":   {"d-1": {constants.FieldID: "d-1"}},
			"Epsilon__c": {"e-1": {constants.FieldID: "e-1"}},
		},
	}

	txm := &recordingBulkTxManager{}
	ps, rollupSvc := newT2bService(repo, txm)

	user := &models.UserSession{ID: "u1"}
	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "T2b test")

	err := ps.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		return ps.runParentLockCoordinatorIfActive(txCtx, func(coordCtx context.Context) error {
			// Step 1: acquire any forced prelocks from the prior retry.
			forcedKeys := parentLockForcedPrelocksFromContext(coordCtx)
			if len(forcedKeys) > 0 {
				if _, lockErr := ps.LockParentRows(coordCtx, forcedKeys); lockErr != nil {
					return lockErr
				}
			}

			// Step 2: REAL rollup derived-write — acquires Zenith__c/z-1 as high-water
			// via prelockInitialUpdateParents on Mid__c (ZenithId__c="z-1" pre-set).
			if rollupErr := rollupSvc.ProcessRollups(coordCtx, "Leaf__c", models.SObject{
				constants.FieldID: "leaf-001",
				"MidId__c":        "mid-001",
				"Amount__c":       float64(5),
			}); rollupErr != nil {
				return rollupErr
			}

			// Step 3: PreviewBeforeUpdate with 5 FK fields all sorting below Zenith__c.
			// On attempt 1 this returns outOfOrderParentLockRetryError{5 keys}.
			// On attempt 2 the forced prelocks include all 5 → already held → nil.
			_, prevErr := ps.PreviewBeforeUpdate(coordCtx, "Leaf__c", "leaf-001",
				models.SObject{
					"AlphaId__c":   "a-1",
					"BetaId__c":    "b-1",
					"GammaId__c":   "g-1",
					"DeltaId__c":   "d-1",
					"EpsilonId__c": "e-1",
				},
				nil, // internalStamps nil (per §2.2 design)
				user,
			)
			return prevErr
		})
	})
	if err != nil {
		t.Fatalf("RunInPessimisticTransaction returned error: %v", err)
	}

	// The mixed rollup+lookup key set merges correctly; converges in 2 total
	// pessimistic boundary invocations (1 retry).
	if txm.pessimisticCalls > 3 {
		t.Fatalf("pessimistic transaction calls = %d, want ≤3 (mixed key merge converges within budget)", txm.pessimisticCalls)
	}
	if txm.pessimisticCalls < 2 {
		t.Fatalf("pessimistic transaction calls = %d, want ≥2 (at least one retry for the out-of-order discovery)", txm.pessimisticCalls)
	}

	// The rollup write must have committed the new total to Mid.
	if got := repo.rows["Mid__c"]["mid-001"]["LeafTotal__c"]; got != float64(5) {
		t.Fatalf("Mid__c.LeafTotal__c = %v, want 5 (rollup derived-write should have committed)", got)
	}
}
