package formula

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/expression"
	"github.com/nexuscrm/kernel/pkg/formula/spec"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestEvaluateDoesNotMutateCallerOwnedPercentMaps(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{{APIName: "Probability", Type: models.FieldType(constants.FieldTypePercent)}},
	}
	record := map[string]interface{}{"Probability": 40}

	got, err := engine.Evaluate(`Probability < 1`, &Context{
		Record: record,
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("Evaluate returned %v, want true after 40 -> 0.4 normalization", got)
	}
	if record["Probability"] != 40 {
		t.Fatalf("caller-owned record mutated to %#v, want storage-domain 40 preserved", record["Probability"])
	}
}

func TestEvaluateNormalizesPercentFieldsFromSchema(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{{APIName: "Probability", Type: models.FieldType(constants.FieldTypePercent)}},
	}

	got, err := engine.Evaluate(`Probability < 1`, &Context{
		Record: map[string]interface{}{"Probability": 40},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("Evaluate returned %v, want true after 40 -> 0.4 normalization", got)
	}
}

func TestEvaluateNormalizesStorageHydratedPercentDecimalCarrier(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{{APIName: "Probability", Type: models.FieldType(constants.FieldTypePercent)}},
	}
	record := schemautil.FromStorageRecord(schema, models.SObject{"Probability": "40.000000000000000000"}, nil)

	got, err := engine.Evaluate(`Probability < 1`, &Context{
		Record: record,
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("Evaluate returned %v, want true after exact decimal storage carrier 40 -> 0.4 normalization", got)
	}
}

func TestEvaluateNormalizesHydratedPercentRollup(t *testing.T) {
	engine := NewEngine()
	percentType := models.FieldType(constants.FieldTypePercent)
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{{
			APIName:    "DiscountRollup",
			Type:       constants.FieldTypeRollupSummary,
			ReturnType: &percentType,
			RollupConfig: &models.RollupConfig{
				ResultType: &percentType,
			},
		}},
	}
	record := schemautil.FromStorageRecord(
		schema,
		models.SObject{"DiscountRollup": "60.000000000000000000"},
		nil,
	)

	got, err := engine.Evaluate(`1 - DiscountRollup`, &Context{
		Record: record,
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	number, ok := got.(float64)
	if !ok {
		t.Fatalf("Evaluate returned %v (%T), want float64", got, got)
	}
	if math.Abs(number-0.4) > 1e-12 {
		t.Fatalf("Evaluate returned %.17g, want 0.4 after stored rollup 60 -> formula-domain 0.60 normalization", number)
	}
}

func TestSafeCompareUsesExactDecimalStrings(t *testing.T) {
	engine := NewEngine()

	got, err := engine.Evaluate(`SAFEGT(left, right)`, &Context{
		Record: map[string]interface{}{
			"left":  "9007199254740993.000000000000000002",
			"right": "9007199254740993.000000000000000001",
		},
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("SAFEGT exact decimal strings = %#v, want true", got)
	}
}

func newEngineWithIncludes(t *testing.T) *Engine {
	t.Helper()
	engine := NewEngine(WithoutRuntimeFunctionBundles())
	if err := engine.RegisterFunctionBundle(testIncludesFunctionBundle(t)); err != nil {
		t.Fatalf("RegisterFunctionBundle INCLUDES: %v", err)
	}
	return engine
}

func TestEvaluateIncludesUsesSchemaForMultipicklistStorageEnvelope(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "Others", Label: "Others"},
			},
		},
	}}

	got, err := engine.Evaluate(`INCLUDES(Field, "Others")`, &Context{
		Record: map[string]interface{}{"Field": `["Others"]`},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES(Field, "Others") = %v, want true for storage envelope`, got)
	}

	got, err = engine.Evaluate(`INCLUDES($Record.Field, "Others")`, &Context{
		Record: map[string]interface{}{"Field": `["Others"]`},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate $Record returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES($Record.Field, "Others") = %v, want true for storage envelope`, got)
	}
}

func TestEvaluateIncludesUsesSchemaForStorageEnvelopeWithStaleSelection(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "A", Label: "A"},
			},
		},
	}}
	ctx := &Context{
		Record: map[string]interface{}{"Field": `["A","Stale"]`},
		Schema: schema,
	}

	got, err := engine.Evaluate(`INCLUDES(Field, "A")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES(Field, "A") = %v, want true for storage envelope with stale selection`, got)
	}

	got, err = engine.Evaluate(`INCLUDES($Record.Field, "Stale")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate $Record stale returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES($Record.Field, "Stale") = %v, want true for undeclared stored selection`, got)
	}
}

func TestEvaluateIncludesSemicolonRHSLiteralUsesANDSemantics(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "A", Label: "A"},
				{Value: "B", Label: "B"},
				{Value: "C", Label: "C"},
			},
		},
	}}

	tests := []struct {
		name   string
		record map[string]interface{}
		want   bool
	}{
		{
			name:   "semicolon stored value contains both tokens",
			record: map[string]interface{}{"Field": "B;A"},
			want:   true,
		},
		{
			name:   "slice value contains both tokens",
			record: map[string]interface{}{"Field": []string{"A", "B"}},
			want:   true,
		},
		{
			name:   "canonical JSON storage contains both tokens",
			record: map[string]interface{}{"Field": `["A","B"]`},
			want:   true,
		},
		{
			name:   "missing one token is false",
			record: map[string]interface{}{"Field": `["A"]`},
			want:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := engine.Evaluate(`INCLUDES(Field, "A;B")`, &Context{
				Record: tt.record,
				Schema: schema,
			})
			if err != nil {
				t.Fatalf("Evaluate returned error: %v", err)
			}
			if got != tt.want {
				t.Fatalf(`INCLUDES(Field, "A;B") = %v, want %v`, got, tt.want)
			}
		})
	}
}

func TestEvaluateIncludesNonLiteralSemicolonRHSKeepsSingleValueSemantics(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "A", Label: "A"},
				{Value: "B", Label: "B"},
			},
		},
		{APIName: "WantedTag__c", Type: models.FieldType(constants.FieldTypeString)},
	}}

	got, err := engine.Evaluate(`INCLUDES(Field, WantedTag__c)`, &Context{
		Record: map[string]interface{}{
			"Field":        `["A","B"]`,
			"WantedTag__c": "A;B",
		},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != false {
		t.Fatalf(`INCLUDES(Field, WantedTag__c) with WantedTag__c="A;B" = %v, want false single-value RHS semantics`, got)
	}
}

func TestEvaluateIncludesSchemaEnvCannotBeShadowedByVariables(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "Others", Label: "Others"},
			},
		},
	}}

	got, err := engine.Evaluate(`INCLUDES(Field, "Others")`, &Context{
		Record: map[string]interface{}{"Field": `["Others"]`},
		Variables: map[string]interface{}{
			expression.MultiPicklistFieldEnvKey: map[string]interface{}{
				"Field": []string{`["Others"]`},
			},
		},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("schema multipicklist metadata was shadowed by variables; got %v, want true", got)
	}
}

func TestEvaluateIncludesBareVariableShadowKeepsVariableSemantics(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "Others", Label: "Others"},
			},
		},
	}}
	ctx := &Context{
		Record:    map[string]interface{}{"Field": `["Others"]`},
		Variables: map[string]interface{}{"Field": `["Others"]`},
		Schema:    schema,
	}

	got, err := engine.Evaluate(`INCLUDES(Field, "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != false {
		t.Fatalf(`INCLUDES(Field, "Others") with variable shadow = %v, want false plain-variable semantics`, got)
	}

	got, err = engine.Evaluate(`INCLUDES($Record.Field, "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate $Record returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES($Record.Field, "Others") = %v, want true for explicit record reference`, got)
	}
}

func TestEvaluateIncludesRecordAndPriorVariableShadowKeepsVariableSemantics(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: "Others", Label: "Others"},
			},
		},
	}}
	ctx := &Context{
		Record: map[string]interface{}{"Field": `["Others"]`},
		Prior:  map[string]interface{}{"Field": `["Others"]`},
		Variables: map[string]interface{}{
			"record": map[string]interface{}{"Field": `["Others"]`},
			"prior":  map[string]interface{}{"Field": `["Others"]`},
		},
		Schema: schema,
	}

	got, err := engine.Evaluate(`INCLUDES(record.Field, "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate record shadow returned error: %v", err)
	}
	if got != false {
		t.Fatalf(`INCLUDES(record.Field, "Others") with variable shadow = %v, want false plain-variable semantics`, got)
	}

	got, err = engine.Evaluate(`INCLUDES(prior.Field, "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate prior shadow returned error: %v", err)
	}
	if got != false {
		t.Fatalf(`INCLUDES(prior.Field, "Others") with variable shadow = %v, want false plain-variable semantics`, got)
	}

	got, err = engine.Evaluate(`INCLUDES($Record.Field, "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate $Record returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES($Record.Field, "Others") = %v, want true for explicit record reference`, got)
	}

	got, err = engine.Evaluate(`INCLUDES(PRIORVALUE(Field), "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate PRIORVALUE returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES(PRIORVALUE(Field), "Others") = %v, want true for explicit prior reference`, got)
	}
}

func TestEvaluateIncludesUsesSchemaForDeclaredJSONLookingMultipicklistOption(t *testing.T) {
	engine := newEngineWithIncludes(t)
	schema := &models.ObjectMetadata{Fields: []models.FieldMetadata{
		{
			APIName: "Field",
			Type:    models.FieldType(constants.FieldTypeMultiPicklist),
			Options: models.PicklistOptions{
				{Value: `["Others"]`, Label: `["Others"]`},
				{Value: "Others", Label: "Others"},
			},
		},
	}}
	ctx := &Context{
		Record: map[string]interface{}{"Field": `["[\"Others\"]"]`},
		Schema: schema,
	}

	got, err := engine.Evaluate(`INCLUDES(Field, "[\"Others\"]")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf(`INCLUDES(Field, "[\"Others\"]") = %v, want true for exact declared option`, got)
	}

	got, err = engine.Evaluate(`INCLUDES(Field, "Others")`, ctx)
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != false {
		t.Fatalf(`INCLUDES(Field, "Others") = %v, want false for distinct option`, got)
	}
}

func TestSafeFormulaCompareRejectsIncomparableTypes(t *testing.T) {
	cmp, ok, err := safeFormulaCompare(true, 1)
	if err != nil {
		t.Fatalf("safeFormulaCompare(true, 1) error = %v", err)
	}
	if ok {
		t.Fatalf("safeFormulaCompare(true, 1) comparable = %v, want false", ok)
	}
	if cmp != 0 {
		t.Fatalf("safeFormulaCompare(true, 1) cmp = %d, want 0", cmp)
	}
}

func TestSafeFormulaCompareSupportsPlainStrings(t *testing.T) {
	cmp, ok, err := safeFormulaCompare("Bravo", "Charlie")
	if err != nil {
		t.Fatalf("safeFormulaCompare(string, string) error = %v", err)
	}
	if !ok {
		t.Fatal("safeFormulaCompare(string, string) comparable = false, want true")
	}
	if cmp >= 0 {
		t.Fatalf("safeFormulaCompare(\"Bravo\", \"Charlie\") cmp = %d, want negative", cmp)
	}
}

func TestSafeFormulaCompareSupportsDateStrings(t *testing.T) {
	cmp, ok, err := safeFormulaCompare("2026-01-01", "2026-06-01")
	if err != nil {
		t.Fatalf("safeFormulaCompare(date, date) error = %v", err)
	}
	if !ok {
		t.Fatal("safeFormulaCompare(date, date) comparable = false, want true")
	}
	if cmp >= 0 {
		t.Fatalf("safeFormulaCompare(\"2026-01-01\", \"2026-06-01\") cmp = %d, want negative", cmp)
	}
}

func TestSafeFormulaCompareTreatsBlankOperandsAsNotComparable(t *testing.T) {
	cmp, ok, err := safeFormulaCompare("", "2026-06-01")
	if err != nil {
		t.Fatalf("safeFormulaCompare(blank, date) error = %v", err)
	}
	if ok {
		t.Fatalf("safeFormulaCompare(blank, date) comparable = %v, want false", ok)
	}
	if cmp != 0 {
		t.Fatalf("safeFormulaCompare(blank, date) cmp = %d, want 0", cmp)
	}
}

func TestStrictSafeCompareTreatsBlankOperandsAsViolation(t *testing.T) {
	engine := NewEngine()

	got, err := engine.Evaluate("STRICTSAFEGT(left, right)", &Context{
		Variables: map[string]interface{}{
			"left":  "",
			"right": "2026-06-01",
		},
	})
	if err != nil {
		t.Fatalf("STRICTSAFEGT eval error = %v", err)
	}
	isTrue, _ := got.(bool)
	if !isTrue {
		t.Fatalf("STRICTSAFEGT(blank, date) = %#v, want true", got)
	}
}

// ---------------------------------------------------------------------------
// T22b — formula.Context.UserSession field round-trip (RED → GREEN)
// ---------------------------------------------------------------------------

// TestContext_UserSession_RoundTrip asserts that formula.Context accepts and
// round-trips a populated *models.UserSession value (B2, B5).
func TestContext_UserSession_RoundTrip(t *testing.T) {
	user := &models.UserSession{
		ID:          "u-rt-001",
		Name:        "Round Trip User",
		ProfileID:   "prof-rt",
		ProfileName: "Admin",
	}
	ctx := &Context{UserSession: user}
	if ctx.UserSession == nil {
		t.Fatal("Context.UserSession is nil after assignment; want non-nil")
	}
	if ctx.UserSession.ID != "u-rt-001" {
		t.Errorf("Context.UserSession.ID = %q; want %q", ctx.UserSession.ID, "u-rt-001")
	}
	if ctx.UserSession.ProfileID != "prof-rt" {
		t.Errorf("Context.UserSession.ProfileID = %q; want %q", ctx.UserSession.ProfileID, "prof-rt")
	}
}

// ---------------------------------------------------------------------------
// LB03 — DATE/DATETIME comparison semantics (Salesforce v63 oracle)
// ---------------------------------------------------------------------------

// TestSafeFormulaCompare_DateDatetimeSemantics pins the Salesforce v63 behavior
// for DATE-DATE, DATETIME-DATETIME, and DATE-DATETIME mixed comparisons.
// Each sub-test is an explicit AAA assertion against the oracle table in
// implementation-notes.md §"Salesforce v63 DATE/DATETIME comparison oracle".
func TestSafeFormulaCompare_DateDatetimeSemantics(t *testing.T) {
	tests := []struct {
		name      string
		left      interface{}
		right     interface{}
		wantCmp   int
		wantOK    bool
		oracleRow int
	}{
		// Case 1 — DATE-DATE: same calendar day → cmp = 0
		{name: "DateDate_Equal", left: "2026-05-06", right: "2026-05-06", wantCmp: 0, wantOK: true, oracleRow: 1},
		// Case 2 — DATE-DATE: left earlier → cmp = -1
		{name: "DateDate_LeftEarlier", left: "2026-05-06", right: "2026-05-07", wantCmp: -1, wantOK: true, oracleRow: 2},
		// Case 3 — DATETIME-DATETIME: UTC instant order → cmp = -1
		{name: "DatetimeDatetime_LeftEarlier", left: "2026-05-06T08:00:00Z", right: "2026-05-06T09:00:00Z", wantCmp: -1, wantOK: true, oracleRow: 3},
		// Case 4 — DATETIME-DATETIME: same UTC instant, different zones → cmp = 0
		{name: "DatetimeDatetime_SameInstantDifferentZones", left: "2026-05-06T08:00:00Z", right: "2026-05-06T16:00:00+08:00", wantCmp: 0, wantOK: true, oracleRow: 4},
		// Case 6 — DATE vs DATETIME same day: DATETIME truncates to UTC calendar date → cmp = 0
		{name: "DateDatetime_Mixed_SameDay", left: "2026-05-06", right: "2026-05-06T23:30:00Z", wantCmp: 0, wantOK: true, oracleRow: 6},
		// Case 7 — DATE vs DATETIME different day: DATETIME truncates to next day → cmp = -1
		{name: "DateDatetime_Mixed_LeftEarlier", left: "2026-05-06", right: "2026-05-07T00:00:00Z", wantCmp: -1, wantOK: true, oracleRow: 7},
		// Case 8 — DATE vs DATETIME: DATE is later by one calendar day → cmp = 1
		{name: "DateDatetime_Mixed_LeftLater", left: "2026-05-07", right: "2026-05-06T23:30:00Z", wantCmp: 1, wantOK: true, oracleRow: 8},
		// Case 9 — blank vs DATE: blank operand → cmp = 0, comparable = false
		{name: "BlankDate", left: "", right: "2026-05-06", wantCmp: 0, wantOK: false, oracleRow: 9},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Arrange — inputs are set in the table above.
			// Act
			gotCmp, gotOK, err := safeFormulaCompare(tc.left, tc.right)
			// Assert
			if err != nil {
				t.Fatalf("oracle row %d: safeFormulaCompare(%q, %q) unexpected error = %v",
					tc.oracleRow, tc.left, tc.right, err)
			}
			if gotOK != tc.wantOK {
				t.Errorf("oracle row %d: comparable = %v, want %v",
					tc.oracleRow, gotOK, tc.wantOK)
			}
			if gotCmp != tc.wantCmp {
				t.Errorf("oracle row %d: cmp = %d, want %d (left=%q right=%q)",
					tc.oracleRow, gotCmp, tc.wantCmp, tc.left, tc.right)
			}
		})
	}
}

// TestSafeFormulaCompare_BareTimeIsAlwaysDatetime closes the IANTHEREAL P1
// against PR #248 (5/5 17:25Z review): the previous parseFormulaComparableTime
// inferred kindDate from a bare time.Time whose clock was exactly 00:00:00Z
// and location == time.UTC. That over-matches legitimate DATETIME values
// landing on midnight UTC — once one side is kindDate, compareFormulaTimes
// truncates BOTH sides to UTC calendar day, so a DATETIME at 2026-05-06T00:00:00Z
// vs a DATETIME later that day returned cmp=0 instead of -1.
//
// Contract going forward: a bare `time.Time` is always DATETIME. Callers that
// hold a schema-known Date field must convert to a "2006-01-02" string before
// invoking the formula engine; the string parser path is the canonical way to
// produce kindDate.
func TestSafeFormulaCompare_BareTimeIsAlwaysDatetime(t *testing.T) {
	midnightUTC := time.Date(2026, 5, 6, 0, 0, 0, 0, time.UTC)

	tests := []struct {
		name    string
		left    interface{}
		right   interface{}
		wantCmp int
	}{
		{
			name:    "MidnightUTC_vs_LaterSameDay",
			left:    midnightUTC,
			right:   "2026-05-06T12:00:00Z",
			wantCmp: -1,
		},
		{
			name:    "LaterSameDay_vs_MidnightUTC",
			left:    "2026-05-06T12:00:00Z",
			right:   midnightUTC,
			wantCmp: 1,
		},
		{
			name:    "MidnightUTC_vs_SameInstantString",
			left:    midnightUTC,
			right:   "2026-05-06T00:00:00Z",
			wantCmp: 0,
		},
		{
			name:    "BothMidnightUTC_DifferentDays",
			left:    midnightUTC,
			right:   time.Date(2026, 5, 7, 0, 0, 0, 0, time.UTC),
			wantCmp: -1,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			gotCmp, gotOK, err := safeFormulaCompare(tc.left, tc.right)
			if err != nil {
				t.Fatalf("safeFormulaCompare(%v, %v) error = %v", tc.left, tc.right, err)
			}
			if !gotOK {
				t.Fatalf("safeFormulaCompare(%v, %v) ok = false; want true", tc.left, tc.right)
			}
			if gotCmp != tc.wantCmp {
				t.Errorf("safeFormulaCompare(%v, %v) cmp = %d, want %d",
					tc.left, tc.right, gotCmp, tc.wantCmp)
			}
		})
	}
}

func TestFormulaCriteriaDateTimeLiteralWorksForEvaluationAndSQL(t *testing.T) {
	const expr = "CreatedDate > '2026-06-01 04:05:00'"

	engine := NewEngine()
	got, err := engine.Evaluate(expr, &Context{
		Record: map[string]interface{}{"CreatedDate": "2026-06-01 04:05:06"},
	})
	if err != nil {
		t.Fatalf("Evaluate(%q) error = %v", expr, err)
	}
	if got != true {
		t.Fatalf("Evaluate(%q) = %v, want true", expr, got)
	}

	sqlWhere, args, err := ToSQL(expr)
	if err != nil {
		t.Fatalf("ToSQL(%q) error = %v", expr, err)
	}
	if !strings.Contains(sqlWhere, "CreatedDate > ?") {
		t.Fatalf("sqlWhere = %q, want CreatedDate comparison", sqlWhere)
	}
	if len(args) != 1 || args[0] != "2026-06-01 04:05:00" {
		t.Fatalf("args = %#v, want backend-comparable datetime literal", args)
	}
}

// TestContext_UserSession_NilSafe asserts that a Context with UserSession == nil
// does not panic during formula evaluation (B6).
func TestContext_UserSession_NilSafe(t *testing.T) {
	engine := NewEngine()
	ctx := &Context{
		Variables: map[string]interface{}{"x": float64(1)},
		// UserSession intentionally nil
	}
	got, err := engine.Evaluate("x + 1", ctx)
	if err != nil {
		t.Fatalf("Evaluate with nil UserSession error = %v", err)
	}
	if got != float64(2) {
		t.Errorf("Evaluate result = %v; want 2", got)
	}
}

// TestValidateSyntax_AcceptsRecordRelationFormulas pins the contract for the
// validation surface used by metadata linters (bootstrap setup verify, schema
// formula-save preflight). Frozen-mode formulas reference sibling record
// fields, $Record relationship paths, and engine builtins that the
// per-record runtime resolves through ctx.Record / ctx.Fetcher / registered
// functions. A validator that fails on any undefined identifier is stricter
// than the runtime evaluator and produces false-positive InvalidFormula
// violations on syntactically correct shipped formulas (Codex finding on
// PR #268: empty-env Engine.Validate rejects $Record.Account.Name and bare
// sibling-field references during setup verify / readiness).
//
// ValidateSyntax MUST accept all of these forms (pure parse-level check) and
// MUST still reject genuine syntax errors so the gate is not vacuous.
func TestValidateSyntax_AcceptsRecordRelationFormulas(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{
			name: "sibling field reference",
			expr: "Amount > 100",
		},
		{
			name: "dollar Record relation path",
			expr: `$Record.Account.Name`,
		},
		{
			name: "ISBLANK on sibling field",
			expr: `ISBLANK(Description)`,
		},
		{
			name: "IF with sibling fields",
			expr: `IF(Amount > 0, "positive", "non-positive")`,
		},
		{
			name: "merge field syntax",
			expr: `{!$Record.Owner.Email}`,
		},
		{
			name: "Salesforce AND/OR infix",
			expr: `Amount > 0 AND Status == "Active"`,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (runtime resolves identifiers per-record)", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_RejectsGenuineSyntaxErrors pins the negative half of the
// contract: lax identifier handling does not turn ValidateSyntax into a
// no-op. Parse-level breakage (unmatched parens, malformed token streams)
// must still surface as InvalidFormula at the validator boundary.
func TestValidateSyntax_RejectsGenuineSyntaxErrors(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{
			name: "unmatched open parens",
			expr: "((((",
		},
		{
			name: "unterminated string literal",
			expr: `"unterminated`,
		},
		{
			name: "trailing operator",
			expr: "Amount +",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err == nil {
				t.Fatalf("ValidateSyntax(%q) error = nil; want non-nil (genuine syntax error)", tc.expr)
			}
		})
	}
}

// TestValidateSyntax_RejectsRuntimeUnsupportedFunctions is the IANTHEREAL P1
// regression on PR #268: Engine.ValidateSyntax used to compile with
// expr.AllowUndefinedVariables() WITHOUT registering the runtime function
// list, so frozen formulas that referenced unimplemented Salesforce functions
// (BLANKVALUE, VLOOKUP, REGEX, ...) silently passed install-pack /
// verify-pack while records.computeFormulaFields then failed at write time
// and stored NULL into the derived column — actual data corruption, not just
// a verification gap.
//
// Each case below is a frozen-formula shape that uses a function the
// runtime evaluator does NOT register. ValidateSyntax MUST reject them so
// the install-lifecycle proof is fail-closed.
func TestValidateSyntax_RejectsRuntimeUnsupportedFunctions(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "VLOOKUP", expr: `VLOOKUP("ZipCode__c", "City__c", "94025")`},
		{name: "REGEX", expr: `REGEX(Description, "^foo")`},
		{name: "EXCLUDES", expr: `EXCLUDES(Multipicklist, "value")`},
		{name: "BR", expr: `BR()`},
		{name: "GEOLOCATION", expr: `GEOLOCATION(37.0, -122.0)`},
		{name: "DISTANCE", expr: `DISTANCE(Loc1, Loc2, "mi")`},
		{name: "CURRENCYRATE", expr: `CURRENCYRATE("USD")`},
		{name: "JSENCODE", expr: `JSENCODE(Name)`},
		{name: "URLENCODE", expr: `URLENCODE(Name)`},
		{name: "HTMLENCODE", expr: `HTMLENCODE(Name)`},
		{name: "ISNUMBER", expr: `ISNUMBER(AccountNumber)`},
		{name: "ISTEXT", expr: `ISTEXT(Name)`},
		{
			name: "double-nested unsupported in argument position",
			expr: `IF(NOT(ISBLANK(Account)), TEXT(VLOOKUP("X", "Y", "Z")), "")`,
		},
		{name: "wholly unknown function", expr: `NO_SUCH_FUNCTION(Amount)`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := engine.ValidateSyntax(tc.expr)
			if err == nil {
				t.Fatalf("ValidateSyntax(%q) error = nil; want non-nil (runtime evaluator does not register this function — frozen-formula evaluation would fail at record write and corrupt the column with NULL)", tc.expr)
			}
		})
	}
}

// TestValidateSyntax_AcceptsIMAGE pins that IMAGE is registered in the frozen
// formula runtime and ValidateSyntax accepts it (not an unsupported function)
// when called within its Salesforce arity bounds. IMAGE(image_url,
// alternate_text [, height [, width]]) → 2-4 args; passthrough returning the URL
// as a string. A 1-arg call is below the Salesforce minimum and rejected
// (IANTHEREAL P1-b, PR #1550).
func imageTestBundle() FunctionBundle {
	return FunctionBundle{
		Functions: []FunctionDefinition{{
			Name:    "IMAGE",
			MinArgs: 2,
			MaxArgs: 4,
			Return:  FunctionReturnDefinition{Kind: "fixed", Type: spec.TypeString},
			Args:    []spec.ArgSpec{{Type: spec.TypeString, Nullable: true}},
			Evaluator: FunctionEvaluator{
				Kind:   FormulaExprV1EvaluatorKind,
				Params: []string{"arg0", "arg1"},
				Body:   "arg0",
			},
		}},
	}
}

func TestValidateSyntax_AcceptsIMAGE(t *testing.T) {
	engine := NewEngine()
	if err := engine.RegisterFunctionBundle(imageTestBundle()); err != nil {
		t.Fatalf("RegisterFunctionBundle IMAGE: %v", err)
	}

	cases := []struct {
		name string
		expr string
	}{
		{name: "IMAGE with 4 args", expr: `IMAGE("https://example.com/logo.png", "alt", 80, 80)`},
		{name: "IMAGE with 2 args", expr: `IMAGE("https://example.com/logo.png", "alt")`},
		{name: "IMAGE lowercase normalized", expr: `image("https://example.com/logo.png", "alt")`},
		{name: "IMAGE in comparison", expr: `IMAGE("https://example.com/x.png", "alt", 80, 80) == "https://example.com/x.png"`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (IMAGE is registered via bundle)", tc.expr, err)
			}
		})
	}

	t.Run("IMAGE with 1 arg rejected below minimum", func(t *testing.T) {
		if err := engine.ValidateSyntax(`IMAGE("https://example.com/logo.png")`); err == nil {
			t.Fatal("ValidateSyntax(IMAGE 1 arg) error = nil; want rejection — Salesforce IMAGE requires >=2 args")
		}
	})
}

// TestEvaluate_IMAGE_Passthrough pins that IMAGE evaluates to the URL string
// in the frozen formula runtime (passthrough semantics matching active compiler).
func TestEvaluate_IMAGE_Passthrough(t *testing.T) {
	engine := NewEngine()
	if err := engine.RegisterFunctionBundle(imageTestBundle()); err != nil {
		t.Fatalf("RegisterFunctionBundle IMAGE: %v", err)
	}

	cases := []struct {
		name string
		expr string
		want string
	}{
		{
			name: "IMAGE with 4 args returns URL",
			expr: `IMAGE("https://example.com/logo.png", "alt", 80, 80)`,
			want: "https://example.com/logo.png",
		},
		{
			name: "IMAGE with 3 args returns URL",
			expr: `IMAGE("https://example.com/logo.png", "alt", 80)`,
			want: "https://example.com/logo.png",
		},
		{
			name: "IMAGE with 2 args returns URL",
			expr: `IMAGE("https://example.com/logo.png", "alt text")`,
			want: "https://example.com/logo.png",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := engine.Evaluate(tc.expr, &Context{})
			if err != nil {
				t.Fatalf("Evaluate(%q) error = %v; want nil", tc.expr, err)
			}
			if got != tc.want {
				t.Fatalf("Evaluate(%q) = %v; want %q", tc.expr, got, tc.want)
			}
		})
	}
}

// TestEvaluate_IMAGE_TooManyArgs pins that the runtime handler rejects more
// than 4 arguments, matching the ValidateSyntax compile-time arity gate. The
// bound is derived from the spec catalog (IMAGE 2-4), so the error renders the
// declared bounds rather than a bespoke message.
func TestEvaluate_IMAGE_TooManyArgs(t *testing.T) {
	engine := NewEngine()
	if err := engine.RegisterFunctionBundle(imageTestBundle()); err != nil {
		t.Fatalf("RegisterFunctionBundle IMAGE: %v", err)
	}

	// 5 args: url, alt, width, height, extra — one beyond the 4-arg maximum.
	_, err := engine.Evaluate(`IMAGE("https://example.com/logo.png", "alt", 1, 2, 3)`, &Context{})
	if err == nil {
		t.Fatal("Evaluate IMAGE with 5 args: error = nil; want non-nil (max 4 args)")
	}
	if !strings.Contains(err.Error(), "2-4 args") {
		t.Fatalf("Evaluate IMAGE with 5 args: error = %q; want message containing \"2-4 args\"", err.Error())
	}
}

func TestValidateSyntax_RejectsUnknownFunctionWithCandidates(t *testing.T) {
	engine := NewEngine()

	err := engine.ValidateSyntax(`FOO(Amount)`)
	if err == nil {
		t.Fatal("ValidateSyntax(FOO(Amount)) error = nil; want non-nil")
	}
	if !strings.Contains(err.Error(), "FOO") {
		t.Fatalf("ValidateSyntax(FOO(Amount)) error = %q; want rejected function name in message", err)
	}
	if !strings.Contains(err.Error(), "did you mean:") {
		t.Fatalf("ValidateSyntax(FOO(Amount)) error = %q; want candidate suggestions in message", err)
	}
}

// TestValidateSyntax_AcceptsRuntimeRegisteredFunctions pins the positive
// half of the function-name gate: every name registered with the runtime
// MUST pass ValidateSyntax.
func TestValidateSyntax_AcceptsRuntimeRegisteredFunctions(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "ABS", expr: `ABS(Amount)`},
		{name: "ISBLANK", expr: `ISBLANK(Description)`},
		{name: "TEXT and CASE", expr: `CASE(TEXT(StageName), "Closed Won", 1, "Closed Lost", 0, -1)`},
		{name: "ROUND", expr: `ROUND(Amount, 2)`},
		{name: "IF nested with builtins", expr: `IF(ISBLANK(Phone), TEXT(Email), TEXT(Phone))`},
		{name: "SAFEGT", expr: `SAFEGT(Amount, 100)`},
		{name: "ISCHANGEDX synthetic", expr: `ISCHANGEDX(_has_prior_, Stage, _prior_Stage, false, false)`},
		{name: "BCRYPT", expr: `BCRYPT(Password__c)`},
		{name: "TODAY arithmetic produces DATE_ADD", expr: `TODAY()+30`},
		{name: "LPAD", expr: `LPAD(Name, 10, "0")`},
		{name: "RPAD", expr: `RPAD(Name, 10, " ")`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (function is registered with the runtime evaluator and frozen-formula evaluation would succeed)", tc.expr, err)
			}
		})
	}
}

func TestValidateSyntax_AcceptsRuntimeNormalizedStandardFunctionCase(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "lowercase abs", expr: `abs(Amount)`},
		{name: "lowercase if", expr: `if(IsActive, 1, 0)`},
		{name: "lowercase case", expr: `case(StageName, "Closed Won", 1, 0)`},
		{name: "lowercase today arithmetic", expr: `today()+30`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (runtime normalization canonicalizes this standard Salesforce function spelling before evaluation)", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_AcceptsLPADRPADNormalizedCase verifies that lowercase and
// mixed-case spellings of LPAD and RPAD are accepted by ValidateSyntax.
// Salesforce formula function names are case-insensitive; the normalizer must
// canonicalize all case variants before the expr-lang parser sees them.
// Regression test for the P1 found in PR #968 gate review: lpad/rpad were
// registered in runtimeFunctionSignatures but missing from functionPatterns.
func TestValidateSyntax_AcceptsLPADRPADNormalizedCase(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "lowercase lpad", expr: `lpad(Name, 5, "0")`},
		{name: "uppercase LPAD", expr: `LPAD(Name, 5, "0")`},
		{name: "mixed-case Lpad", expr: `Lpad(Name, 3, "x")`},
		{name: "lowercase rpad", expr: `rpad(Name, 10, " ")`},
		{name: "uppercase RPAD", expr: `RPAD(Name, 10, " ")`},
		{name: "mixed-case Rpad", expr: `Rpad(Name, 8, "-")`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (LPAD/RPAD are registered in the runtime and all case variants must be normalized)", tc.expr, err)
			}
		})
	}
}

func TestValidateSyntax_PreservesUndefinedIdentifierToleranceForNonCalls(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "bare field identifiers", expr: `Amount + Discount__c`},
		{name: "relationship field identifiers", expr: `Parent__r.Name & " / " & Owner.Name`},
		{name: "identifiers inside registered call", expr: `IF(StageName == "Closed Won", Amount, ForecastAmount)`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (undefined identifiers remain runtime-resolved; only call-shape names are tightened)", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_RejectsCaseVariants pins the fail-closed contract for
// function names the runtime does NOT canonicalize before expr-lang dispatch.
func TestValidateSyntax_RejectsCaseVariants(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "lowercase bcrypt", expr: `bcrypt(Password__c)`},
		{name: "MixedCase Bcrypt", expr: `Bcrypt(Password__c)`},
		{name: "lowercase safelt", expr: `safelt(Amount, 100)`},
		{name: "lowercase strictsafelt", expr: `strictsafelt(Amount, 100)`},
		{name: "lowercase ischangedx", expr: `ischangedx(_has_prior_, Stage, _prior_Stage, false, false)`},
		{name: "lowercase truthy", expr: `truthy(IsActive)`},
		{name: "lowercase date_add", expr: `date_add(TODAY(), 30)`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := engine.ValidateSyntax(tc.expr)
			if err == nil {
				t.Fatalf("ValidateSyntax(%q) error = nil; want non-nil (case-variant function name — runtime evaluator's expr.Function registration is case-sensitive; would fail at record-write time and write NULL)", tc.expr)
			}
		})
	}
}

// TestValidateSyntax_AcceptsExactCase pins the positive half of case-sensitivity:
// canonical upper-case spelling of every extension MUST clear the walker.
func TestValidateSyntax_AcceptsExactCase(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "BCRYPT exact case", expr: `BCRYPT(Password__c)`},
		{name: "SAFELT exact case", expr: `SAFELT(Amount, 100)`},
		{name: "SAFEGT exact case", expr: `SAFEGT(Amount, 100)`},
		{name: "STRICTSAFELT exact case", expr: `STRICTSAFELT(Amount, 100)`},
		{name: "STRICTSAFEGTE exact case", expr: `STRICTSAFEGTE(Amount, 100)`},
		{name: "ISCHANGEDX exact case", expr: `ISCHANGEDX(_has_prior_, Stage, _prior_Stage, false, false)`},
		{name: "TRUTHY exact case", expr: `TRUTHY(IsActive)`},
		{name: "DATE_ADD exact case", expr: `DATE_ADD(TODAY(), 30)`},
		{name: "ISNULLX exact case", expr: `ISNULLX(Amount, false)`},
		{name: "ISBLANKX exact case", expr: `ISBLANKX(Phone, false)`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (registered runtime function in its canonical case must clear the walker)", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_RejectsMethodCallShapes pins the IANTHEREAL R9 P1
// regression: any CallNode whose Callee is NOT *ast.IdentifierNode must be
// rejected. The runtime evaluator does not register methods on user data;
// attempting to dispatch a method on a reflected user value triggers
// reflect.Value.Call panics.
func TestValidateSyntax_RejectsMethodCallShapes(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "Account.Name()", expr: `Account.Name()`},
		{name: "Foo.BAR(Amount)", expr: `Foo.BAR(Amount)`},
		{name: "IF wrapping method-call", expr: `IF(true, Account.Name(), "fallback")`},
		{name: "$Record method-call", expr: `$Record.Account.IsActive()`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := engine.ValidateSyntax(tc.expr)
			if err == nil {
				t.Fatalf("ValidateSyntax(%q) error = nil; want non-nil (method-call shape — runtime evaluator does not register methods on user data and would panic at reflect.Value.Call)", tc.expr)
			}
		})
	}
}

// TestVerdict_ZeroValueIsUnverified pins the structural fail-closed-by-
// default contract: a Verdict that has not been explicitly set MUST default
// to Unverified, never Verified.
func TestVerdict_ZeroValueIsUnverified(t *testing.T) {
	var v Verdict
	if v == Verified {
		t.Fatal("zero-value Verdict equals Verified; fail-closed-by-default contract is broken — uninitialised verdicts MUST block install")
	}
	if v != Unverified {
		t.Fatalf("zero-value Verdict = %v; want Unverified (the install gate's structural invariant)", v)
	}
}

// TestValidateSyntax_RejectsArityMismatch pins the Round-11 static replacement
// for the previous DryEvaluate-against-stubs arity gate.
func TestValidateSyntax_RejectsArityMismatch(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "IF arity 1", expr: `IF(Amount)`},
		{name: "IF arity 2", expr: `IF(true, 1)`},
		{name: "NOT arity 0", expr: `NOT()`},
		{name: "ISBLANK arity 0", expr: `ISBLANK()`},
		{name: "AND arity 0", expr: `AND()`},
		{name: "OR arity 0", expr: `OR()`},
		{name: "IF arity 4", expr: `IF(true, 1, 2, 3)`},
		{name: "ROUND arity 3", expr: `ROUND(Amount, 2, "extra")`},
		{name: "FIND arity 4", expr: `FIND("x", "abc", 1, "extra")`},
		// CASE even-count gate (IANTHEREAL P1, PR #308): odd-arg CASE passes
		// runtime's `len(params) < 4` check but fails `len(params)%2 != 0`.
		// The static gate must reject odd-arg CASE so Verified never returns for
		// a formula that always errors at runtime.
		{name: "CASE odd args 5", expr: `CASE(Status, "A", "x", "B", "y")`},
		{name: "CASE odd args 3", expr: `CASE(Status, "A", "x")`},
		{name: "CASE odd args 7", expr: `CASE(Status, "A", 1, "B", 2, "C", 3)`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := engine.ValidateSyntax(tc.expr)
			if err == nil {
				t.Fatalf("ValidateSyntax(%q) error = nil; want non-nil (arity mismatch must be statically rejected — runtime evaluator's `expr.Function` registration is signature-less and would fail at record-write time, corrupting the derived column with NULL)", tc.expr)
			}
		})
	}
}

// TestValidateSyntax_AcceptsCorrectArity pins the positive half of the
// arity gate.
func TestValidateSyntax_AcceptsCorrectArity(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "IF exact arity 3", expr: `IF(true, 1, 2)`},
		{name: "ROUND exact arity 2", expr: `ROUND(Amount, 2)`},
		{name: "ISCHANGEDX exact arity 5", expr: `ISCHANGEDX(_has_prior_, Stage, _prior_Stage, false, false)`},
		{name: "AND with one arg", expr: `AND(IsActive)`},
		{name: "OR with two args", expr: `OR(IsActive, IsDeleted)`},
		{name: "FIND arity 2", expr: `FIND("x", "abc")`},
		{name: "FIND arity 3", expr: `FIND("x", "abc", 1)`},
		{name: "SUBSTITUTE arity 3", expr: `SUBSTITUTE("abc", "a", "z")`},
		{name: "SUBSTITUTE arity 4", expr: `SUBSTITUTE("aaa", "a", "z", 2)`},
		{name: "TODAY arity 0", expr: `TODAY()`},
		{name: "NOW arity 0", expr: `NOW()`},
		// CASE even-arg forms must pass the static gate.
		{name: "CASE 4 args", expr: `CASE(Status, "A", 1, 0)`},
		{name: "CASE 6 args", expr: `CASE(Status, "A", 1, "B", 2, 0)`},
		{name: "CASE 8 args", expr: `CASE(Status, "A", 1, "B", 2, "C", 3, 0)`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (call satisfies the declared [Min, Max] arity bounds; static gate must not falsely reject it)", tc.expr, err)
			}
		})
	}
}

func TestValidateSyntax_AcceptsIncludesSemicolonRHSLiteral(t *testing.T) {
	engine := newEngineWithIncludes(t)

	accepted := []string{
		`INCLUDES(Tags__c, "A;B")`,
		`includes(Tags__c, "A;B")`,
		`INCLUDES(Tags__c, "A")`,
		`INCLUDES(Tags__c, "[\"Others\"]")`,
		`INCLUDES(Tags__c, WantedTag__c)`,
	}
	for _, expr := range accepted {
		t.Run(expr, func(t *testing.T) {
			if err := engine.ValidateSyntax(expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) = %v; want nil", expr, err)
			}
		})
	}
}

// TestValidateSyntax_AcceptsTypedFieldExpressions is the regression test for
// IANTHEREAL Round-11 P1 #1: the previous DryEvaluate-against-stubs verify
// gate rejected legal formulas like `Amount > 0`, `Amount + Total`,
// `Email & Phone` that evaluate cleanly at record-write time.
func TestValidateSyntax_AcceptsTypedFieldExpressions(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "Amount > 0", expr: `Amount > 0`},
		{name: "Amount + Total", expr: `Amount + Total`},
		{name: "Email & Phone", expr: `Email & Phone`},
		{name: "string equality on sibling", expr: `Stage == "Closed Won"`},
		{name: "boolean infix on sibling", expr: `IsActive AND Amount > 100`},
		{name: "$Record dotted path arithmetic", expr: `$Record.Account.AnnualRevenue * 0.1`},
		{name: "ISBLANK on sibling field", expr: `ISBLANK(Description)`},
		{name: "IF over typed compare", expr: `IF(Amount > 0, "positive", "non-positive")`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (typed-field expression evaluates cleanly at runtime; static gate must not falsely reject — IANTHEREAL Round-11 P1 #1 regression)", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_RejectsNonRecordGlobalRoots pins the IANTHEREAL P1
// findings from PR #308 R2 (2026-05-08T01:44Z, $Permission) and R3
// (2026-05-08T03:25Z, $Organization/$Setup/$Label). All `$X → __X`
// normalized roots other than `__Record` must be rejected at verify because
// computeFormulaFields builds formula.Context with only Record + Fetcher
// and Engine.Evaluate only injects `__Record` into the eval env. A
// previous denylist of "known session-bound globals" left every other `$X`
// passing silently — a fresh round of review kept finding new roots
// ($Organization, $Setup, $Label, $Api, …) that the runtime evaluator
// stores as NULL. The static gate now uses an allowlist: only `__Record`
// is accepted; everything else is Failed.
func TestValidateSyntax_RejectsNonRecordGlobalRoots(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		// R2 — session-bound globals (PR #308 round-2 P1).
		{name: "$Permission bare", expr: `$Permission.ActivateOrder`},
		{name: "$Permission in IF", expr: `IF($Permission.ActivateOrder, "yes", "no")`},
		{name: "$User bare", expr: `$User.Email`},
		{name: "$User in concat", expr: `$User.FirstName & " " & $User.LastName`},
		{name: "$Profile bare", expr: `$Profile.Name`},
		{name: "$UserRole bare", expr: `$UserRole.Name`},
		// R3 — non-session globals the denylist missed (PR #308 round-3 P1).
		{name: "$Organization bare", expr: `$Organization.Name`},
		{name: "$Setup field", expr: `$Setup.SomeFlag__c`},
		{name: "$Label bare", expr: `$Label.SomeText`},
		{name: "$Api bare", expr: `$Api.Session_ID`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := engine.ValidateSyntax(tc.expr)
			if err == nil {
				t.Fatalf("ValidateSyntax(%q) = nil; want error (non-Record global not injected by computeFormulaFields — IANTHEREAL P1 PR #308 R2/R3)", tc.expr)
			}
			if !strings.Contains(err.Error(), "global root") {
				t.Fatalf("ValidateSyntax(%q) error = %q; want error containing 'global root'", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_AcceptsRecordGlobals confirms that $Record paths —
// which the runtime resolves via formula.Context.Record — are NOT rejected
// by the session-global check. $Record is not a session-bound global; it is
// the frozen formula field's own record context.
func TestValidateSyntax_AcceptsRecordGlobals(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "$Record.Amount arithmetic", expr: `$Record.Amount * 1.1`},
		{name: "$Record cross-object", expr: `$Record.Account.AnnualRevenue`},
		{name: "$Record nested IF", expr: `IF($Record.IsClosed, "closed", "open")`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil ($Record is the record context, not a session global)", tc.expr, err)
			}
		})
	}
}

// TestValidateSyntax_AcceptsRelationshipRootsNamedLikeSessionGlobals guards
// the distinction between normalized session globals (`__Profile`, `__User`,
// ...) and ordinary relationship traversals whose root identifier happens to
// share the same bare name. Frozen formulas may legally traverse
// `Profile.Name` / `UserRole.Name` when those are real relationship roots on
// the record being evaluated; only `$Profile` / `$UserRole` should be
// rejected by the session-global gate.
func TestValidateSyntax_AcceptsRelationshipRootsNamedLikeSessionGlobals(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "Profile relationship", expr: `Profile.Name`},
		{name: "UserRole relationship", expr: `UserRole.Name`},
		{name: "Permission relationship", expr: `Permission.Name`},
		{name: "User relationship", expr: `User.Email`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (bare relationship root must not be misclassified as a session global)", tc.expr, err)
			}
		})
	}
}

// ---------------------------------------------------------------------------
// DATETIMEVALUE — frozen runtime implementation (issue: datetimevalue-frozen)
// ---------------------------------------------------------------------------

// TestDATETIMEVALUE_ValidateSyntax_Accepted pins that DATETIMEVALUE is
// accepted by ValidateSyntax as a runtime-registered function.
func TestDATETIMEVALUE_ValidateSyntax_Accepted(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "uppercase field arg", expr: `DATETIMEVALUE(CreatedDate)`},
		{name: "string literal arg", expr: `DATETIMEVALUE("2020-01-15 16:00:00")`},
		{name: "lowercase spelling normalized", expr: `datetimevalue(CreatedDate)`},
		{name: "nested in IF", expr: `IF(ISBLANK(CreatedDate), NOW(), DATETIMEVALUE(CreatedDate))`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (DATETIMEVALUE is registered with the runtime evaluator)", tc.expr, err)
			}
		})
	}
}

// TestDATETIMEVALUE_Evaluate_DatetimeString parses "YYYY-MM-DD HH:MM:SS"
// into a value that the expression engine can return and compare.
func TestDATETIMEVALUE_Evaluate_DatetimeString(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name      string
		expr      string
		record    map[string]interface{}
		wantEmpty bool // true when Salesforce returns null (parse failure)
	}{
		{
			name:   "ISO datetime string literal",
			expr:   `DATETIMEVALUE("2020-01-15 16:00:00")`,
			record: map[string]interface{}{},
		},
		{
			name:   "date-only string literal",
			expr:   `DATETIMEVALUE("2020-01-15")`,
			record: map[string]interface{}{},
		},
		{
			name:   "field containing datetime string",
			expr:   `DATETIMEVALUE(CreatedDate)`,
			record: map[string]interface{}{"CreatedDate": "2024-06-01 08:30:00"},
		},
		{
			name:      "invalid string returns nil (Salesforce null)",
			expr:      `DATETIMEVALUE("not-a-date")`,
			record:    map[string]interface{}{},
			wantEmpty: true,
		},
		{
			name:      "integer input returns nil (Salesforce: non-string → null)",
			expr:      `DATETIMEVALUE(Amount)`,
			record:    map[string]interface{}{"Amount": 1},
			wantEmpty: true,
		},
		{
			name:      "float input returns nil (Salesforce: non-string → null)",
			expr:      `DATETIMEVALUE(Amount)`,
			record:    map[string]interface{}{"Amount": 1.5},
			wantEmpty: true,
		},
		{
			name:   "time.Time field passes through unchanged (existing record hydrated value)",
			expr:   `DATETIMEVALUE(CreatedDate)`,
			record: map[string]interface{}{"CreatedDate": time.Date(2020, 1, 15, 16, 0, 0, 0, time.UTC)},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := engine.Evaluate(tc.expr, &Context{Record: tc.record})
			if err != nil {
				t.Fatalf("Evaluate(%q) error = %v; want nil", tc.expr, err)
			}
			if tc.wantEmpty {
				if got != nil {
					t.Fatalf("Evaluate(%q) = %v; want nil for invalid input", tc.expr, got)
				}
				return
			}
			if got == nil {
				t.Fatalf("Evaluate(%q) = nil; want non-nil datetime value", tc.expr)
			}
			// Result must always be time.Time regardless of input representation.
			v, ok := got.(time.Time)
			if !ok {
				t.Fatalf("Evaluate(%q) = %v (%T); want time.Time", tc.expr, got, got)
			}
			if v.IsZero() {
				t.Fatalf("Evaluate(%q) returned zero time.Time; want parsed datetime", tc.expr)
			}
		})
	}
}

func TestDATETIMEFROMEPOCHMILLIS_Evaluate(t *testing.T) {
	engine := NewEngine()
	const milliseconds = int64(1783774384356)
	want := time.UnixMilli(milliseconds).UTC()

	cases := []struct {
		name      string
		expr      string
		record    map[string]interface{}
		wantNil   bool
		wantError bool
	}{
		{name: "integer literal", expr: `DATETIMEFROMEPOCHMILLIS(1783774384356)`},
		{name: "decoded JSON float", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": float64(milliseconds)}},
		{name: "json number", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": json.Number("1783774384356")}},
		{name: "connector numeric string", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": "1783774384356"}},
		{name: "nil preserves null", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": nil}, wantNil: true},
		{name: "fractional input rejected", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": 1.5}, wantError: true},
		{name: "fractional string input rejected", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": "1783774384356.5"}, wantError: true},
		{name: "non-numeric string input rejected", expr: `DATETIMEFROMEPOCHMILLIS(OccurredAt)`, record: map[string]interface{}{"OccurredAt": "not-an-epoch"}, wantError: true},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := engine.Evaluate(tc.expr, &Context{Record: tc.record})
			if tc.wantError {
				if err == nil {
					t.Fatalf("Evaluate(%q) error = nil; want invalid epoch-millisecond input rejection", tc.expr)
				}
				return
			}
			if err != nil {
				t.Fatalf("Evaluate(%q) error = %v; want nil", tc.expr, err)
			}
			if tc.wantNil {
				if got != nil {
					t.Fatalf("Evaluate(%q) = %v; want nil", tc.expr, got)
				}
				return
			}
			gotTime, ok := got.(time.Time)
			if !ok {
				t.Fatalf("Evaluate(%q) = %v (%T); want time.Time", tc.expr, got, got)
			}
			if !gotTime.Equal(want) {
				t.Fatalf("Evaluate(%q) = %s; want %s", tc.expr, gotTime, want)
			}
		})
	}
}

func TestDATETIMEFROMEPOCHMILLIS_ValidateSyntaxWithSchema(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		APIName: "HubSpotContact",
		Fields: []models.FieldMetadata{
			{APIName: "OccurredAt", Type: constants.FieldTypeString},
			{APIName: "Amount", Type: constants.FieldTypeDouble},
			{APIName: "IsActive", Type: constants.FieldTypeBoolean},
			{APIName: "CloseDate", Type: constants.FieldTypeDate},
			{APIName: "CreatedDate", Type: constants.FieldTypeDatetime},
		},
	}

	for _, expr := range []string{
		`DATETIMEFROMEPOCHMILLIS(OccurredAt)`,
		`DATETIMEFROMEPOCHMILLIS(Amount)`,
		`DATETIMEFROMEPOCHMILLIS("1783774384356")`,
	} {
		if err := engine.ValidateSyntaxWithSchema(expr, schema); err != nil {
			t.Fatalf("ValidateSyntaxWithSchema(%q) error = %v; want nil", expr, err)
		}
	}

	for _, expr := range []string{
		`DATETIMEFROMEPOCHMILLIS("1783774384356.5")`,
		`DATETIMEFROMEPOCHMILLIS("not-an-epoch")`,
		`DATETIMEFROMEPOCHMILLIS(IsActive)`,
		`DATETIMEFROMEPOCHMILLIS(CloseDate)`,
		`DATETIMEFROMEPOCHMILLIS(CreatedDate)`,
	} {
		if err := engine.ValidateSyntaxWithSchema(expr, schema); err == nil {
			t.Fatalf("ValidateSyntaxWithSchema(%q) error = nil; want epoch-millisecond type rejection", expr)
		}
	}
}

// TestDATETIMEVALUE_ValidateSyntaxWithSchema_Accepted pins that
// DATETIMEVALUE is accepted by the schema-aware validator when given a
// DateTime field in the schema.
func TestDATETIMEVALUE_ValidateSyntaxWithSchema_Accepted(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "CreatedDate", Type: constants.FieldTypeDatetime},
			{APIName: "BirthDate", Type: constants.FieldTypeDate},
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}

	cases := []struct {
		name string
		expr string
	}{
		{name: "DateTime field arg", expr: `DATETIMEVALUE(CreatedDate)`},
		{name: "Date field arg", expr: `DATETIMEVALUE(BirthDate)`},
		{name: "string field arg", expr: `DATETIMEVALUE(Name)`},
		{name: "string literal arg", expr: `DATETIMEVALUE("2020-01-15 16:00:00")`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntaxWithSchema(tc.expr, schema); err != nil {
				t.Fatalf("ValidateSyntaxWithSchema(%q) error = %v; want nil", tc.expr, err)
			}
		})
	}
}

// ---------------------------------------------------------------------------
// Fact 2 — multi-hop chain augmentation (issue #571)
// ---------------------------------------------------------------------------

// buildChainFetcher returns a fetcher that resolves relationships by walking
// the supplied adjacency map: map[objectType]map[relationName]relatedRecord.
// It stamps attributes.type on each returned record so intermediate hops
// can identify the current object type.
func buildChainFetcher(graph map[string]map[string]map[string]interface{}) func(map[string]interface{}, string) (map[string]interface{}, error) {
	return func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
		// Identify the current object from attributes.type.
		var objectType string
		if attrs, ok := record["attributes"].(map[string]interface{}); ok {
			objectType, _ = attrs["type"].(string)
		}
		related, ok := graph[objectType][relationName]
		if !ok {
			return nil, nil // definitive null
		}
		return related, nil
	}
}

// TestFormulaEvaluate_MultiHopChain_TwoLevel verifies that a 2-hop chain
// $Record.A__r.B__r.Name is resolved by augmentRecordWithRelationChain and
// the leaf value is returned by Evaluate.
func TestFormulaEvaluate_MultiHopChain_TwoLevel(t *testing.T) {
	// Graph: Root → A__r (Account) → B__r (Contact) with Name="leaf-two"
	graph := map[string]map[string]map[string]interface{}{
		"Root": {
			"A__r": {
				"attributes": map[string]interface{}{"type": "Account"},
				"Id":         "acc-001",
			},
		},
		"Account": {
			"B__r": {
				"attributes": map[string]interface{}{"type": "Contact"},
				"Name":       "leaf-two",
			},
		},
	}

	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"attributes": map[string]interface{}{"type": "Root"},
		},
		Fetcher: buildChainFetcher(graph),
	}

	// Expression uses Salesforce $Record. form.
	got, err := engine.Evaluate("$Record.A__r.B__r.Name", ctx)
	if err != nil {
		t.Fatalf("Evaluate() error = %v", err)
	}
	if got != "leaf-two" {
		t.Fatalf("Evaluate() = %v, want %q", got, "leaf-two")
	}
}

// TestFormulaEvaluate_MultiHopChain_ThreeLevel verifies a 3-hop chain resolves
// to the correct leaf value.
func TestFormulaEvaluate_MultiHopChain_ThreeLevel(t *testing.T) {
	graph := map[string]map[string]map[string]interface{}{
		"Root": {
			"A__r": {
				"attributes": map[string]interface{}{"type": "A"},
				"Id":         "a-001",
			},
		},
		"A": {
			"B__r": {
				"attributes": map[string]interface{}{"type": "B"},
				"Id":         "b-001",
			},
		},
		"B": {
			"C__r": {
				"attributes": map[string]interface{}{"type": "C"},
				"Name":       "leaf-three",
			},
		},
	}

	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"attributes": map[string]interface{}{"type": "Root"},
		},
		Fetcher: buildChainFetcher(graph),
	}

	got, err := engine.Evaluate("$Record.A__r.B__r.C__r.Name", ctx)
	if err != nil {
		t.Fatalf("Evaluate() error = %v", err)
	}
	if got != "leaf-three" {
		t.Fatalf("Evaluate() = %v, want %q", got, "leaf-three")
	}
}

// TestFormulaEvaluate_MultiHopChain_NullIntermediate verifies that a mid-chain
// definitive null (fetcher returns nil, nil) causes the expression to evaluate
// to nil rather than panic.
func TestFormulaEvaluate_MultiHopChain_NullIntermediate(t *testing.T) {
	// A__r resolves, but B__r returns nil (definitive null).
	fetcher := func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
		if relationName == "A__r" {
			return map[string]interface{}{
				"attributes": map[string]interface{}{"type": "Account"},
				"Id":         "acc-001",
			}, nil
		}
		return nil, nil // definitive null for B__r
	}

	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"attributes": map[string]interface{}{"type": "Root"},
		},
		Fetcher: fetcher,
	}

	// With B__r null, the leaf field Name should resolve to nil.
	got, err := engine.Evaluate("$Record.A__r.B__r.Name", ctx)
	if err != nil {
		t.Fatalf("Evaluate() error = %v, want nil (null intermediate = nil result)", err)
	}
	if got != nil {
		t.Fatalf("Evaluate() = %v, want nil for null intermediate hop", got)
	}
}

// TestFormulaEvaluate_MultiHopChain_FetcherError verifies that a fetcher error
// on a multi-hop chain is wrapped as ErrRelationFetch.
func TestFormulaEvaluate_MultiHopChain_FetcherError(t *testing.T) {
	fetcher := func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
		return nil, fmt.Errorf("transient DB error")
	}

	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"attributes": map[string]interface{}{"type": "Root"},
		},
		Fetcher: fetcher,
	}

	_, err := engine.Evaluate("$Record.A__r.B__r.Name", ctx)
	if err == nil {
		t.Fatal("Evaluate() error = nil, want ErrRelationFetch-wrapped error")
	}
	if !errors.Is(err, ErrRelationFetch) {
		t.Fatalf("Evaluate() error = %v, want wrapped ErrRelationFetch", err)
	}
}

func TestFormulaEvaluate_RelationPrefetchSkipsISNEWDeadBranch(t *testing.T) {
	var calls []string
	fetcher := func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
		calls = append(calls, relationName)
		return map[string]interface{}{"Name": "should-not-load"}, nil
	}

	engine := NewEngine()
	got, err := engine.Evaluate(`
		AND(
			ISNEW(),
			NOT(
				AND(
					NOT(ISNEW()),
					$Record.CreatedBy.Profile.Name == "Sales"
				)
			)
		)
	`, &Context{
		Record:  map[string]interface{}{"Company": "Acme"},
		Fetcher: fetcher,
	})
	if err != nil {
		t.Fatalf("Evaluate() error = %v", err)
	}
	if got != true {
		t.Fatalf("Evaluate() = %v, want true", got)
	}
	if len(calls) != 0 {
		t.Fatalf("Fetcher calls = %v, want none for create-only live branch", calls)
	}
}

func TestFormulaEvaluate_RelationPrefetchKeepsISNEWLiveUpdateBranch(t *testing.T) {
	var calls []string
	fetcher := func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
		calls = append(calls, relationName)
		switch relationName {
		case "CreatedBy":
			return map[string]interface{}{"Id": "005-created-by"}, nil
		case "Profile":
			return map[string]interface{}{"Name": "Sales"}, nil
		default:
			return nil, fmt.Errorf("unexpected relation %q", relationName)
		}
	}

	engine := NewEngine()
	got, err := engine.Evaluate(`
		AND(
			NOT(ISNEW()),
			$Record.CreatedBy.Profile.Name == "Sales"
		)
	`, &Context{
		Record:  map[string]interface{}{"Company": "Acme"},
		Prior:   map[string]interface{}{"Company": "Old Acme"},
		Fetcher: fetcher,
	})
	if err != nil {
		t.Fatalf("Evaluate() error = %v", err)
	}
	if got != true {
		t.Fatalf("Evaluate() = %v, want true", got)
	}
	if len(calls) != 2 || calls[0] != "CreatedBy" || calls[1] != "Profile" {
		t.Fatalf("Fetcher calls = %v, want [CreatedBy Profile]", calls)
	}
}

// TestFormulaEvaluate_SingleHopRelation_NoRegression verifies that the existing
// single-hop behavior is unaffected by the multi-hop augmentation.
func TestFormulaEvaluate_SingleHopRelation_NoRegression(t *testing.T) {
	fetcher := func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
		if relationName == "Account__r" {
			return map[string]interface{}{"Name": "Acme Corp"}, nil
		}
		return nil, nil
	}

	engine := NewEngine()
	ctx := &Context{
		Record:  map[string]interface{}{"Account__c": "acc-001"},
		Fetcher: fetcher,
	}

	got, err := engine.Evaluate("$Record.Account__r.Name", ctx)
	if err != nil {
		t.Fatalf("Evaluate() error = %v", err)
	}
	if got != "Acme Corp" {
		t.Fatalf("Evaluate() = %v, want %q", got, "Acme Corp")
	}
}

// TestExtractRecordRelations_RecordsSidePathUnchanged verifies that
// extractRecordRelations returns only the single-hop relation from a mixed
// expression, and extractRecordRelationChains returns only the multi-hop chain.
func TestExtractRecordRelations_RecordsSidePathUnchanged(t *testing.T) {
	// Expression has both a single-hop (A__r) and a two-hop chain (X__r.Y__r).
	// After normalization, $Record becomes __Record.
	expr := normalizeFormulaExpression("$Record.A__r.Name + $Record.X__r.Y__r.Leaf")

	singleHops := extractRecordRelations(expr)
	// Should include both A__r and X__r (first segment of the chain)
	found := make(map[string]bool)
	for _, r := range singleHops {
		found[r] = true
	}
	if !found["A__r"] {
		t.Errorf("extractRecordRelations missing A__r; got %v", singleHops)
	}
	if !found["X__r"] {
		t.Errorf("extractRecordRelations missing X__r (first hop of chain); got %v", singleHops)
	}

	chains := extractRecordRelationChains(expr)
	if len(chains) == 0 {
		t.Fatal("extractRecordRelationChains returned no chains; want [X__r, Y__r]")
	}
	// Find the chain [X__r, Y__r]
	var foundChain bool
	for _, chain := range chains {
		if len(chain) == 2 && chain[0] == "X__r" && chain[1] == "Y__r" {
			foundChain = true
			break
		}
	}
	if !foundChain {
		t.Errorf("extractRecordRelationChains missing [X__r, Y__r]; got %v", chains)
	}
}

// ---------------------------------------------------------------------------
// BLANKVALUE frozen runtime support
// ---------------------------------------------------------------------------

// TestValidateSyntax_AcceptsBLANKVALUE verifies that BLANKVALUE is accepted
// by ValidateSyntax once it is registered with the runtime evaluator.
func TestValidateSyntax_AcceptsBLANKVALUE(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name string
		expr string
	}{
		{name: "blank string returns default", expr: `BLANKVALUE(Phone, "n/a")`},
		{name: "nested in IF", expr: `IF(ISBLANK(ContactId), BLANKVALUE(Phone, "n/a"), Phone)`},
		{name: "cross-object field", expr: `BLANKVALUE(Lead.Phone, Lead.MobilePhone)`},
		{name: "lowercase blankvalue is normalized", expr: `blankvalue(Phone, "n/a")`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) error = %v; want nil (BLANKVALUE is registered with the runtime evaluator)", tc.expr, err)
			}
		})
	}
}

// TestEvaluate_BLANKVALUEReturnsDefaultForBlankValue verifies that BLANKVALUE
// returns the default argument when the first argument is blank or nil.
func TestEvaluate_BLANKVALUEReturnsDefaultForBlankValue(t *testing.T) {
	engine := NewEngine()

	cases := []struct {
		name   string
		expr   string
		record map[string]interface{}
		want   interface{}
	}{
		{
			name:   "nil value returns default string",
			expr:   `BLANKVALUE(Phone, "n/a")`,
			record: map[string]interface{}{"Phone": nil},
			want:   "n/a",
		},
		{
			name:   "empty string returns default",
			expr:   `BLANKVALUE(Phone, "n/a")`,
			record: map[string]interface{}{"Phone": ""},
			want:   "n/a",
		},
		{
			name:   "non-blank string returns original value",
			expr:   `BLANKVALUE(Phone, "n/a")`,
			record: map[string]interface{}{"Phone": "555-1234"},
			want:   "555-1234",
		},
		{
			name:   "absent key treated as nil returns default",
			expr:   `BLANKVALUE(Phone, "n/a")`,
			record: map[string]interface{}{},
			want:   "n/a",
		},
		{
			name:   "default is also a field reference",
			expr:   `BLANKVALUE(Phone, MobilePhone)`,
			record: map[string]interface{}{"Phone": "", "MobilePhone": "555-9999"},
			want:   "555-9999",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := engine.Evaluate(tc.expr, &Context{Record: tc.record})
			if err != nil {
				t.Fatalf("Evaluate(%q) error = %v; want nil", tc.expr, err)
			}
			if got != tc.want {
				t.Fatalf("Evaluate(%q) = %v (%T); want %v (%T)", tc.expr, got, got, tc.want, tc.want)
			}
		})
	}
}

// TestNormalizeFormulaExpression_PreservesSpacePadLiteral verifies that
// normalizeFormulaExpression does not collapse a single-space string literal
// " " used as the pad argument in RPAD/LPAD. This is a regression test for
// the P1 bug where strings.Fields ran before extractStringLiterals, causing
// the literal to be rewritten as "".
func TestNormalizeFormulaExpression_PreservesSpacePadLiteral(t *testing.T) {
	cases := []struct {
		name  string
		input string
		// contains checks that the returned expression contains this substring
		contains string
	}{
		{
			name:     "RPAD double-quoted single space pad is preserved",
			input:    `RPAD(Name, 8, " ")`,
			contains: `" "`,
		},
		{
			name:     "LPAD single-quoted single space pad is preserved",
			input:    `LPAD(Name, 8, ' ')`,
			contains: `' '`,
		},
		{
			name:     "multi-space pad literal is preserved",
			input:    `RPAD(Name, 12, "   ")`,
			contains: `"   "`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := normalizeFormulaExpression(tc.input)
			if !strings.Contains(got, tc.contains) {
				t.Errorf("normalizeFormulaExpression(%q) = %q; expected it to contain %q", tc.input, got, tc.contains)
			}
		})
	}
}

// TestRPAD_SpacePadEndToEnd verifies that a formula using RPAD with a space
// pad literal evaluates correctly through the full formula engine, confirming
// that the normalizer fix flows through to runtime evaluation.
func TestRPAD_SpacePadEndToEnd(t *testing.T) {
	engine := NewEngine()
	got, err := engine.Evaluate(`RPAD(Name, 8, " ")`, &Context{
		Record: map[string]interface{}{"Name": "abc"},
	})
	if err != nil {
		t.Fatalf("Evaluate RPAD space-pad: unexpected error %v", err)
	}
	want := "abc     "
	if got != want {
		t.Fatalf("Evaluate RPAD space-pad = %q; want %q", got, want)
	}
}

// TestLPAD_SpacePadEndToEnd verifies that a formula using LPAD with a space
// pad literal evaluates correctly through the full formula engine.
func TestLPAD_SpacePadEndToEnd(t *testing.T) {
	engine := NewEngine()
	got, err := engine.Evaluate(`LPAD(Name, 8, " ")`, &Context{
		Record: map[string]interface{}{"Name": "abc"},
	})
	if err != nil {
		t.Fatalf("Evaluate LPAD space-pad: unexpected error %v", err)
	}
	want := "     abc"
	if got != want {
		t.Fatalf("Evaluate LPAD space-pad = %q; want %q", got, want)
	}
}

func TestEvaluatePreservesNilPercentForBlankChecks(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: "Probability", Type: models.FieldType(constants.FieldTypePercent)},
		},
	}

	got, err := engine.Evaluate(`ISBLANK(Probability)`, &Context{
		Record: map[string]interface{}{"Probability": nil},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("ISBLANK(Probability) = %v, want true when storage value is nil", got)
	}
}

func TestEvaluatePreservesNilPriorForChangedChecks(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: "Probability", Type: models.FieldType(constants.FieldTypePercent)},
		},
	}

	got, err := engine.Evaluate(`ISCHANGED(Probability)`, &Context{
		Record: map[string]interface{}{"Probability": 40.0},
		Prior:  map[string]interface{}{"Probability": nil},
		Schema: schema,
	})
	if err != nil {
		t.Fatalf("Evaluate returned error: %v", err)
	}
	if got != true {
		t.Fatalf("ISCHANGED(Probability) = %v, want true when prior is nil and current is non-nil", got)
	}
}

// TestForceNotNew_ScheduledGate verifies Bug 1 fix: on a non-record-trigger gate
// (scheduled scan) where Prior==nil, ISNEW() must resolve to false, not true.
// Comment-injection variant (ISNEW/*x*/()) must also be treated as not-new.
func TestForceNotNew_ScheduledGate(t *testing.T) {
	engine := NewEngine()

	t.Run("ISNEW_is_false_with_ForceNotNew", func(t *testing.T) {
		got, err := engine.Evaluate(`ISNEW()`, &Context{
			Record:      map[string]interface{}{"Name": "Acme"},
			Prior:       nil, // no prior — simulates scheduled scan
			ForceNotNew: true,
		})
		if err != nil {
			t.Fatalf("Evaluate returned error: %v", err)
		}
		if got != false {
			t.Fatalf("ISNEW() with ForceNotNew=true got %v, want false", got)
		}
	})

	t.Run("ISNEW_comment_injection_is_false_with_ForceNotNew", func(t *testing.T) {
		// ISNEW/*x*/() — after block-comment stripping this becomes ISNEW () which
		// normalizeFormulaExpression rewrites to _is_new_. ForceNotNew must override
		// that sentinel to false.
		got, err := engine.Evaluate(`ISNEW/*x*/()`, &Context{
			Record:      map[string]interface{}{"Name": "Acme"},
			Prior:       nil,
			ForceNotNew: true,
		})
		if err != nil {
			t.Fatalf("Evaluate (comment-injection) returned error: %v", err)
		}
		if got != false {
			t.Fatalf("ISNEW/*x*/() with ForceNotNew=true got %v, want false", got)
		}
	})

	t.Run("ISNEW_is_true_on_real_create", func(t *testing.T) {
		// Record-trigger create: Prior==nil AND ForceNotNew=false → _is_new_=true.
		got, err := engine.Evaluate(`ISNEW()`, &Context{
			Record:      map[string]interface{}{"Name": "Acme"},
			Prior:       nil,
			ForceNotNew: false,
		})
		if err != nil {
			t.Fatalf("Evaluate returned error: %v", err)
		}
		if got != true {
			t.Fatalf("ISNEW() on real create (Prior==nil, ForceNotNew=false) got %v, want true", got)
		}
	})
}

// TestISCHANGED_MultiPicklist_SetEquality verifies Bug 2 fix: ISCHANGED on a
// multi-picklist field uses set-compare so reordering is not treated as a change.
func TestISCHANGED_MultiPicklist_SetEquality(t *testing.T) {
	engine := NewEngine()
	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: "Tags__c", Type: models.FieldType(constants.FieldTypeMultiPicklist)},
		},
	}

	t.Run("reorder_is_not_changed", func(t *testing.T) {
		got, err := engine.Evaluate(`ISCHANGED(Tags__c)`, &Context{
			Record: map[string]interface{}{"Tags__c": "a;b"},
			Prior:  map[string]interface{}{"Tags__c": "b;a"},
			Schema: schema,
		})
		if err != nil {
			t.Fatalf("Evaluate returned error: %v", err)
		}
		if got != false {
			t.Fatalf("ISCHANGED(Tags__c) reorder a;b→b;a got %v, want false", got)
		}
	})

	t.Run("real_change_is_changed", func(t *testing.T) {
		got, err := engine.Evaluate(`ISCHANGED(Tags__c)`, &Context{
			Record: map[string]interface{}{"Tags__c": "a;b"},
			Prior:  map[string]interface{}{"Tags__c": "a"},
			Schema: schema,
		})
		if err != nil {
			t.Fatalf("Evaluate returned error: %v", err)
		}
		if got != true {
			t.Fatalf("ISCHANGED(Tags__c) a→a;b got %v, want true", got)
		}
	})

	// comment_injection_reorder_is_not_changed verifies the comment-proof fix:
	// ISCHANGED/*c*/(MPField) must NOT bypass the set-compare lowering.
	// The old pre-normalize string rewrite didn't match the commented form, causing
	// normalizeFormulaExpression's inline `!=` expansion to treat "a;b"≠"b;a".
	// The new in-normalize lowering runs after blockCommentPattern strips comments,
	// so the commented form is handled identically to the plain form. Issue #1684.
	t.Run("comment_injection_reorder_is_not_changed", func(t *testing.T) {
		got, err := engine.Evaluate(`ISCHANGED/*c*/(Tags__c)`, &Context{
			Record: map[string]interface{}{"Tags__c": "a;b"},
			Prior:  map[string]interface{}{"Tags__c": "b;a"},
			Schema: schema,
		})
		if err != nil {
			t.Fatalf("Evaluate returned error: %v", err)
		}
		if got != false {
			t.Fatalf("ISCHANGED/*c*/(Tags__c) reorder a;b→b;a got %v, want false (comment-proof)", got)
		}
	})
}

// TestValidateSyntax_RejectsInternalLoweringArtifacts pins public-core-boundary
// invariant: internal lowering tokens (__NCRM_* / __ncrm_*) must not be
// acceptable in author-written formula text.  They are emitted by
// normalizeFormulaExpression as ISCHANGED lowering output and are runtime-only.
// Exposing them as a saveable public contract would turn NexusCRM-internal
// implementation details into a non-Salesforce formula surface.
//
// Probe-confirmed (Gate A CI mirror): ValidateSyntax previously returned nil
// for __NCRM_FIELDUNCHANGED__(...) because it was registered in spec.DefaultRegistry.
func TestValidateSyntax_RejectsInternalLoweringArtifacts(t *testing.T) {
	engine := NewEngine()

	// Author-written uses of internal tokens must be REJECTED.
	rejected := []struct {
		name string
		expr string
	}{
		{
			name: "NCRM_FIELDUNCHANGED direct call",
			expr: `__NCRM_FIELDUNCHANGED__("X", X, X, X)`,
		},
		{
			name: "ncrm_multipicklist_fields global reference",
			expr: `__ncrm_multipicklist_fields != null`,
		},
		{
			name: "NCRM prefix token embedded in expression",
			expr: `IF(__NCRM_FIELDUNCHANGED__("Foo", Foo, Foo, x), "a", "b")`,
		},
	}
	for _, tc := range rejected {
		t.Run(tc.name, func(t *testing.T) {
			err := engine.ValidateSyntax(tc.expr)
			if err == nil {
				t.Fatalf("ValidateSyntax(%q) = nil; want error (internal lowering artifact must be rejected in author input)", tc.expr)
			}
		})
	}

	// ISCHANGED authored by an admin must still be ACCEPTED — it lowers to
	// __NCRM_FIELDUNCHANGED__ internally but the author never wrote the internal token.
	accepted := []struct {
		name string
		expr string
	}{
		{name: "ISCHANGED standard field", expr: `ISCHANGED(Name)`},
		{name: "ISCHANGED custom field", expr: `ISCHANGED(Status__c)`},
		// Reserved prefix appearing as literal DATA (not an identifier) must be
		// ACCEPTED — the raw-source guard must not over-match inside string literals
		// (codex #1733 round-12).
		{name: "reserved prefix inside single-quoted literal", expr: `Status = '__NCRM_FIELDUNCHANGED__'`},
		{name: "reserved prefix inside double-quoted literal", expr: `Status = "__NCRM_X__"`},
		{name: "reserved prefix as function string arg", expr: `CONTAINS(Name, '__ncrm_')`},
	}
	for _, tc := range accepted {
		t.Run(tc.name, func(t *testing.T) {
			if err := engine.ValidateSyntax(tc.expr); err != nil {
				t.Fatalf("ValidateSyntax(%q) = %v; want nil (ISCHANGED is a legitimate author-facing function)", tc.expr, err)
			}
		})
	}
}

// TestEngineValidateRejectsInternalLoweringArtifact verifies that Engine.Validate
