package automation

// executor_active_formula_chained_reread_test.go — regression lock for the
// active-formula re-projection fix introduced in issue #1362 HIGH 3.
//
// When a record-triggered flow runs an in-flow RecordUpdate on the trigger
// record and a later element reads $Record.<active_formula_field>, the fix
// re-projects active-formula values immediately after the raw merge so the
// post-update snapshot is available to subsequent Decision/Assignment/
// FlowFormula reads.
//
// This test LOCKS THE FIX. If executor_record_ops.go is ever changed to skip
// the re-projection after in-flow RecordUpdate, $Record.StatusActive__c will
// revert to the pre-trigger snapshot value ("Cold") and this test will fail,
// surfacing the regression before merge.

import (
	"context"
	"encoding/json"
	"testing"

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

// stubPersistenceCaptureWithReproject wraps stubPersistenceCapture and
// overrides RecomputeActiveFormulaProjection to return a computed snapshot
// based on the Score__c value that was most recently written. This simulates
// the view re-evaluating the formula after the write.
type stubPersistenceCaptureWithReproject struct {
	*stubPersistenceCapture
	// recomputeSnap is returned verbatim on the next RecomputeActiveFormulaProjection
	// call. Set it to the expected post-update active-field snapshot.
	recomputeSnap models.SObject
}

func (s *stubPersistenceCaptureWithReproject) RecomputeActiveFormulaProjection(_ context.Context, _ *models.UserSession, _, _ string, _ []string) (models.SObject, error) {
	return s.recomputeSnap, nil
}

// TestActiveFormula_ChainedReread_RecomputedAfterInFlowUpdate (Pattern B —
// unit test using stub-func / stub-persistence pattern).
//
// Flow structure:
//
//	Element 1: RecordUpdate $Record → write Score__c=99
//	The re-projection stub returns StatusActive__c="Hot" (simulating
//	IF(Score__c>=50,'Hot','Cold') evaluated with Score=99).
//
// Assertion: after executeRecordUpdate merges Score__c=99 AND re-projects
// active-formula fields, $Record.StatusActive__c is "Hot" (the post-update
// recomputed value), NOT "Cold" (the stale pre-trigger snapshot).
func TestActiveFormula_ChainedReread_RecomputedAfterInFlowUpdate(t *testing.T) {
	// Seed $Record with the pre-trigger snapshot already overlaid (simulating
	// what FlowTriggerListener stamps when it builds the inputs map).
	record := models.SObject{
		constants.FieldID: "001x",
		"Score__c":        float64(20),
		"StatusActive__c": "Cold", // stamped from active-formula snapshot at trigger time
		"attributes":      map[string]interface{}{"type": "Widget__c"},
	}

	// Build a flow element: RecordUpdate $Record, write Score__c=99.
	var flowMeta models.FlowMetadata
	if err := json.Unmarshal([]byte(`{
		"recordUpdates": [{
			"name": "Update_Score",
			"object": "Widget__c",
			"inputReference": "$Record",
			"inputAssignments": [
				{"field": "Score__c", "value": {"numberValue": 99}}
			],
			"connector": {"targetReference": "Done"}
		}]
	}`), &flowMeta); err != nil {
		t.Fatalf("unmarshal flow metadata: %v", err)
	}
	if len(flowMeta.RecordUpdates) != 1 {
		t.Fatalf("recordUpdates len = %d, want 1", len(flowMeta.RecordUpdates))
	}

	cap := &captureUpdate{}
	// Use the reproject stub so RecomputeActiveFormulaProjection returns the
	// post-update snapshot (IF(Score__c>=50,'Hot','Cold') with Score=99 → "Hot").
	exec := &FlowExecutor{
		persistence: &stubPersistenceCaptureWithReproject{
			stubPersistenceCapture: &stubPersistenceCapture{capture: cap},
			recomputeSnap:          models.SObject{"StatusActive__c": "Hot"},
		},
	}
	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"$Record": record,
		},
	}

	nextElem, err := exec.executeRecordUpdate(context.Background(), interview, nil, &flowMeta.RecordUpdates[0], nil)
	if err != nil {
		t.Fatalf("executeRecordUpdate: %v", err)
	}
	if nextElem != "Done" {
		t.Fatalf("nextElem = %q, want %q", nextElem, "Done")
	}

	// Confirm the persistence call had Score__c=99.
	if len(cap.calls) != 1 {
		t.Fatalf("persistence.Update called %d times, want 1", len(cap.calls))
	}
	if v := cap.calls[0].updates["Score__c"]; v != float64(99) {
		t.Fatalf("persistence update Score__c = %v, want 99", v)
	}

	rec, _ := interview.ContextData["$Record"].(models.SObject)
	if rec == nil {
		t.Fatal("$Record in ContextData is nil after executeRecordUpdate")
	}
	// The raw merge must have applied the write.
	if v, _ := rec["Score__c"].(float64); v != 99 {
		t.Errorf("$Record.Score__c: want 99 after merge, got %v", v)
	}
	// CRITICAL ASSERTION (locks the fix): after the re-projection, the active
	// field must reflect the post-update computed value, not the stale snapshot.
	// If executor_record_ops.go is changed to skip RecomputeActiveFormulaProjection
	// after the raw merge, StatusActive__c will revert to "Cold" and this test
	// will fail — the implementer must then update this file and the contract note.
	if v, _ := rec["StatusActive__c"].(string); v != "Hot" {
		t.Errorf(
			"$Record.StatusActive__c: want 'Hot' (post-update recomputed value), got %q — "+
				"this means executor_record_ops.go no longer re-projects active fields "+
				"after in-flow RecordUpdate; check the issue #1362 HIGH 3 fix",
			v,
		)
	}
}

// TestActiveFormula_ChainedReread_RecomputedAfterFilteredSelfUpdate locks the
// SAME contract for the filtered-query branch of executeRecordUpdate (the
// Object + Filters shape with no InputReference). When the filter matches
// the triggering record, the same post-update re-projection MUST run so
// downstream reads of $Record.<active> see post-update values. Codex Pass A
// R2 #1381 HIGH surfaced this gap — the original fix only covered the
// single-record $Record path.
//
// If executor_record_ops.go (around line 433) is changed to skip the
// RecomputeActiveFormulaProjection call in the filtered-query branch when
// id == triggerRecordID, this test fails.
func TestActiveFormula_ChainedReread_RecomputedAfterFilteredSelfUpdate(t *testing.T) {
	// Seed $Record with pre-trigger snapshot already overlaid.
	record := models.SObject{
		constants.FieldID: "001x",
		"Score__c":        float64(20),
		"StatusActive__c": "Cold",
		"attributes":      map[string]interface{}{"type": "Widget__c"},
	}

	// Filtered-query RecordUpdate: object="Widget__c", filters target Id=001x
	// (same as $Record), inputAssignments writes Score__c=99.
	var flowMeta models.FlowMetadata
	if err := json.Unmarshal([]byte(`{
		"recordUpdates": [{
			"name": "Update_Score_Filtered",
			"object": "Widget__c",
			"filters": [
				{"field": "Id", "operator": "EqualTo", "value": {"stringValue": "001x"}}
			],
			"inputAssignments": [
				{"field": "Score__c", "value": {"numberValue": 99}}
			],
			"connector": {"targetReference": "Done"}
		}]
	}`), &flowMeta); err != nil {
		t.Fatalf("unmarshal flow metadata: %v", err)
	}

	cap := &captureUpdate{}
	// Query stub returns the trigger record so the filtered-query branch
	// enters the "id == triggerRecordID" path where the reproject must fire.
	queryStub := &limitCapturingQuery{
		result: &models.QueryResult{
			Records: []models.SObject{{constants.FieldID: "001x"}},
		},
	}
	exec := &FlowExecutor{
		persistence: &stubPersistenceCaptureWithReproject{
			stubPersistenceCapture: &stubPersistenceCapture{capture: cap},
			recomputeSnap:          models.SObject{"StatusActive__c": "Hot"},
		},
		query: queryStub,
	}
	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"$Record": record,
		},
	}

	nextElem, err := exec.executeRecordUpdate(context.Background(), interview, nil, &flowMeta.RecordUpdates[0], nil)
	if err != nil {
		t.Fatalf("executeRecordUpdate (filtered): %v", err)
	}
	if nextElem != "Done" {
		t.Fatalf("nextElem = %q, want %q", nextElem, "Done")
	}

	rec, _ := interview.ContextData["$Record"].(models.SObject)
	if rec == nil {
		t.Fatal("$Record nil after filtered-query update")
	}
	if v, _ := rec["Score__c"].(float64); v != 99 {
		t.Errorf("$Record.Score__c (filtered): want 99, got %v", v)
	}
	if v, _ := rec["StatusActive__c"].(string); v != "Hot" {
		t.Errorf(
			"$Record.StatusActive__c (filtered): want 'Hot', got %q — "+
				"filtered-query self-update path is no longer re-projecting "+
				"active fields; check executor_record_ops.go around line 433",
			v,
		)
	}
}
