package formula

import (
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	tests := []struct {
		name       string
		expression string
		context    *Context
		expected   interface{}
	}{
		{
			name:       "addition",
			expression: "1+2",
			context:    &Context{},
			expected:   3,
		},
		{
			name:       "subtraction",
			expression: "10-5",
			context:    &Context{},
			expected:   5,
		},
		{
			name:       "multiplication",
			expression: "4*5",
			context:    &Context{},
			expected:   20,
		},
		{
			name:       "division",
			expression: "100/4",
			context:    &Context{},
			expected:   float64(25),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, tt.context)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	ctx := &Context{
		Record: map[string]interface{}{
			"Amount":    100.0,
			"Discount":  10.0,
			"FirstName": "John",
			"LastName":  "Doe",
		},
	}

	tests := []struct {
		name       string
		expression string
		expected   interface{}
	}{
		{
			name:       "simple field access",
			expression: "record.Amount",
			expected:   100.0,
		},
		{
			name:       "field arithmetic",
			expression: "record.Amount*1.1",
			expected:   110.0,
		},
		{
			name:       "string concatenation",
			expression: "record.FirstName+record.LastName",
			expected:   "JohnDoe",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, ctx)
			assert.NoError(t, err)
			// Use InDelta for float comparisons to handle floating-point precision
			if expectedFloat, ok := tt.expected.(float64); ok {
				if resultFloat, ok := result.(float64); ok {
					assert.InDelta(t, expectedFloat, resultFloat, 0.0001)
					return
				}
			}
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestFormulaEngine_NilStringLiteralEquality(t *testing.T) {
	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"Region__c": nil,
			"Contracting_Entity_EMEA_Only__c": nil,
			"Probability": 0.2,
		},
	}

	tests := []struct {
		name       string
		expression string
		expected   bool
	}{
		{
			name:       "nil field equals string literal returns false",
			expression: `Region__c == "EMEA"`,
			expected:   false,
		},
		{
			name:       "nil field not equals string literal returns true",
			expression: `Region__c != "EMEA"`,
			expected:   true,
		},
		{
			name:       "string literal equals nil field returns false",
			expression: `"EMEA" == Region__c`,
			expected:   false,
		},
		{
			name:       "string literal not equals nil field returns true",
			expression: `"EMEA" != Region__c`,
			expected:   true,
		},
		{
			name:       "nil first conjunct short-circuits validation rule formula",
			expression: `Region__c=='EMEA'&&ISBLANK(TEXT(Contracting_Entity_EMEA_Only__c))&&Probability>0.1`,
			expected:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, ctx)
			require.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	tests := []struct {
		name       string
		expression string
		checkType  bool // If true, just check type, not exact value
	}{
		{
			name:       "TODAY function",
			expression: "TODAY()",
			checkType:  true,
		},
		{
			name:       "NOW function",
			expression: "NOW()",
			checkType:  true,
		},
		{
			name:       "UPPER function",
			expression: "UPPER('hello')",
		},
		{
			name:       "LOWER function",
			expression: "LOWER('WORLD')",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, &Context{})
			assert.NoError(t, err)
			assert.NotNil(t, result)

			if tt.name == "UPPER function" {
				assert.Equal(t, "HELLO", result)
			}
			if tt.name == "LOWER function" {
				assert.Equal(t, "world", result)
			}
		})
	}
}

func TestFormulaEngine_SalesforceSyntaxCompatibility(t *testing.T) {
	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"BillingCountry": "China",
			"Website":        "https://www.example.com/path",
			"FirstName":      "Howard",
			"LastName":       "Cheng",
			"Owner": map[string]interface{}{
				"Profile": map[string]interface{}{
					"Name": "Sales Manager",
				},
				"IsActive": true,
			},
			"Account": map[string]interface{}{
				"Region__c": "APAC",
			},
			"Account__r": map[string]interface{}{
				"Name": "PatSnap",
			},
			"Account__c":  nil,
			"CloseDate":   "2026-03-17",
			"CreatedDate": "2026-03-17T00:00:00Z",
		},
	}

	tests := []struct {
		name       string
		expression string
		expected   interface{}
	}{
		{
			name:       "single equals becomes comparison",
			expression: `IF(BillingCountry="China", "China Mainland", BillingCountry)`,
			expected:   "China Mainland",
		},
		{
			name:       "lowercase text function",
			expression: `text(BillingCountry)`,
			expected:   "China",
		},
		{
			name:       "OR and BEGINS function",
			expression: `IF(OR(BEGINS(Website, "http://www"), BEGINS(Website, "https://www")), "yes", "no")`,
			expected:   "yes",
		},
		{
			name:       "FIND function is 1-based",
			expression: `FIND("/", "abc/def")`,
			expected:   4,
		},
		{
			name:       "ampersand concatenation",
			expression: `FirstName & " " & LastName`,
			expected:   "Howard Cheng",
		},
		{
			name:       "typed owner relation",
			expression: `Owner:User.Profile.Name`,
			expected:   "Sales Manager",
		},
		{
			name:       "custom relationship chain",
			expression: `Account__r.Name`,
			expected:   "PatSnap",
		},
		{
			name:       "datevalue function",
			expression: `DATEVALUE(CreatedDate)`,
			expected:   "2026-03-17",
		},
		{
			name:       "addmonths and year",
			expression: `YEAR( ADDMONTHS(CloseDate, 9) )`,
			expected:   2026,
		},
		{
			name:       "hyperlink returns label",
			expression: `HYPERLINK("/x", "Label")`,
			expected:   "Label",
		},
		{
			name:       "block comments are ignored",
			expression: `IF(ISBLANK(Account__c), /* comment */ "Lead", "Account")`,
			expected:   "Lead",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, ctx)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	tests := []struct {
		name       string
		expression string
		ctx        *Context
		expected   interface{}
	}{
		{
			name:       "NOT true returns false",
			expression: "NOT(true)",
			ctx:        &Context{},
			expected:   false,
		},
		{
			name:       "NOT false returns true",
			expression: "NOT(false)",
			ctx:        &Context{},
			expected:   true,
		},
		{
			name:       "NOT with field comparison",
			expression: `NOT(Status == "Closed")`,
			ctx:        &Context{Record: map[string]interface{}{"Status": "Open"}},
			expected:   true,
		},
		{
			name:       "NOT with CONTAINS",
			expression: `NOT(CONTAINS(Name, "Admin"))`,
			ctx:        &Context{Record: map[string]interface{}{"Name": "Regular User"}},
			expected:   true,
		},
		{
			name:       "NOT with CONTAINS matching",
			expression: `NOT(CONTAINS(Name, "Admin"))`,
			ctx:        &Context{Record: map[string]interface{}{"Name": "System Administrator"}},
			expected:   false,
		},
		{
			name:       "Nested NOT in AND",
			expression: `AND(NOT(CONTAINS(Name, "Admin")), Status == "Open")`,
			ctx: &Context{Record: map[string]interface{}{
				"Name":   "Regular User",
				"Status": "Open",
			}},
			expected: true,
		},
		{
			name:       "lowercase not is normalized",
			expression: `not(true)`,
			ctx:        &Context{},
			expected:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, tt.ctx)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	tests := []struct {
		name       string
		expression string
		ctx        *Context
		expected   interface{}
	}{
		{
			name:       "ISCHANGED detects change",
			expression: "ISCHANGED(StageName)",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Closed Won"},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
			},
			expected: true,
		},
		{
			name:       "ISCHANGED no change",
			expression: "ISCHANGED(StageName)",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Prospecting"},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
			},
			expected: false,
		},
		{
			name:       "ISCHANGED on insert (no prior) returns false",
			expression: "ISCHANGED(StageName)",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Prospecting"},
			},
			expected: false,
		},
		{
			name:       "ISCHANGED in AND expression",
			expression: `AND(ISCHANGED(StageName), TEXT(StageName) == "Closed Won")`,
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Closed Won"},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
			},
			expected: true,
		},
		{
			name:       "lowercase ischanged is normalized",
			expression: "ischanged(Amount)",
			ctx: &Context{
				Record: map[string]interface{}{"Amount": 200.0},
				Prior:  map[string]interface{}{"Amount": 100.0},
			},
			expected: true,
		},
		{
			name:       "ISCHANGED with __Record prefix (flow formula context)",
			expression: "ISCHANGED(__Record.Account__c)",
			ctx: &Context{
				Record: map[string]interface{}{"Account__c": "001NEW"},
				Prior:  map[string]interface{}{"Account__c": "001OLD"},
			},
			expected: true,
		},
		{
			name:       "ISCHANGED with __Record prefix no change",
			expression: "ISCHANGED(__Record.Account__c)",
			ctx: &Context{
				Record: map[string]interface{}{"Account__c": "001SAME"},
				Prior:  map[string]interface{}{"Account__c": "001SAME"},
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, tt.ctx)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	tests := []struct {
		name       string
		expression string
		ctx        *Context
		expected   interface{}
	}{
		{
			name:       "PRIORVALUE returns old value",
			expression: `PRIORVALUE(StageName)`,
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Closed Won"},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
			},
			expected: "Prospecting",
		},
		{
			name:       "PRIORVALUE on insert returns nil",
			expression: `PRIORVALUE(StageName) == nil`,
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Prospecting"},
			},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, tt.ctx)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	tests := []struct {
		name       string
		expression string
		ctx        *Context
		expected   interface{}
	}{
		{
			name:       "$Profile.Name resolves",
			expression: `CONTAINS($Profile.Name, "System Administrator")`,
			ctx: &Context{
				User: map[string]interface{}{
					"__Profile": map[string]interface{}{"Name": "System Administrator", "Id": "prof-001"},
				},
			},
			expected: true,
		},
		{
			name:       "$UserRole.Name resolves",
			expression: `$UserRole.Name == "CEO"`,
			ctx: &Context{
				User: map[string]interface{}{
					"__UserRole": map[string]interface{}{"Name": "CEO", "Id": "role-001"},
				},
			},
			expected: true,
		},
		{
			name:       "$User.Id resolves",
			expression: `$User.Id == "user-123"`,
			ctx: &Context{
				User: map[string]interface{}{
					"__User": map[string]interface{}{"Id": "user-123", "Name": "Admin"},
				},
			},
			expected: true,
		},
		{
			name: "NOT with $Profile.Name in AllowCloseWon-style rule",
			expression: `AND(
				NOT(CONTAINS($Profile.Name, "System Administrator")),
				TEXT(StageName) == "Closed Won"
			)`,
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Closed Won"},
				User: map[string]interface{}{
					"__Profile": map[string]interface{}{"Name": "Standard User"},
				},
			},
			expected: true,
		},
		{
			name: "System Admin bypasses AllowCloseWon-style rule",
			expression: `AND(
				NOT(CONTAINS($Profile.Name, "System Administrator")),
				TEXT(StageName) == "Closed Won"
			)`,
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Closed Won"},
				User: map[string]interface{}{
					"__Profile": map[string]interface{}{"Name": "System Administrator"},
				},
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, tt.ctx)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	fullFormula := `AND(
		NOT(CONTAINS($Profile.Name, "System Administrator")),
		TEXT(StageName) == "Deal Closed",
		ISCHANGED(StageName),
		OR(
			AND(
				Allow_Close__c == true,
				NOT(
					OR(
						CONTAINS($Profile.Name, "DM"),
						CONTAINS($UserRole.Name, "Finance"),
						$UserRole.Name == "Business Ops"
					)
				)
			),
			Allow_Close__c == false
		)
	)`

	tests := []struct {
		name     string
		ctx      *Context
		expected bool
	}{
		{
			name: "System Admin is exempt",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Deal Closed", "Allow_Close__c": false},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
				User: map[string]interface{}{
					"__Profile":  map[string]interface{}{"Name": "System Administrator"},
					"__UserRole": map[string]interface{}{"Name": "CEO"},
				},
			},
			expected: false,
		},
		{
			name: "Non-admin closing without allow = blocked",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Deal Closed", "Allow_Close__c": false},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
				User: map[string]interface{}{
					"__Profile":  map[string]interface{}{"Name": "Standard User"},
					"__UserRole": map[string]interface{}{"Name": "Sales Rep"},
				},
			},
			expected: true,
		},
		{
			name: "Finance role with allow = not blocked",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Deal Closed", "Allow_Close__c": true},
				Prior:  map[string]interface{}{"StageName": "Prospecting"},
				User: map[string]interface{}{
					"__Profile":  map[string]interface{}{"Name": "Standard User"},
					"__UserRole": map[string]interface{}{"Name": "Finance Manager"},
				},
			},
			expected: false,
		},
		{
			name: "Stage not changed = not blocked",
			ctx: &Context{
				Record: map[string]interface{}{"StageName": "Deal Closed", "Allow_Close__c": false},
				Prior:  map[string]interface{}{"StageName": "Deal Closed"},
				User: map[string]interface{}{
					"__Profile":  map[string]interface{}{"Name": "Standard User"},
					"__UserRole": map[string]interface{}{"Name": "Sales Rep"},
				},
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(fullFormula, tt.ctx)
			assert.NoError(t, err, "Formula should evaluate without error")
			boolResult, ok := result.(bool)
			assert.True(t, ok, "Result should be boolean")
			assert.Equal(t, tt.expected, boolResult)
		})
	}
}

func BenchmarkFormulaEvaluate(b *testing.B) {
	engine := NewEngine()
	ctx := &Context{
		Record: map[string]interface{}{
			"Amount": 100.0,
		},
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = engine.Evaluate("record.Amount*1.1", ctx)
	}
}

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

	ctx := &Context{
		Variables: map[string]interface{}{
			"LoopItem": map[string]interface{}{
				"Name":  "Item 1",
				"Value": 100.0,
			},
			"Counter": 5,
		},
	}

	tests := []struct {
		name       string
		expression string
		expected   interface{}
	}{
		{
			name:       "access variable",
			expression: "var.Counter",
			expected:   5,
		},
		{
			name:       "access nested variable",
			expression: "var.LoopItem.Name",
			expected:   "Item 1",
		},
		{
			name:       "variable arithmetic",
			expression: "var.LoopItem.Value * 2",
			expected:   200.0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, ctx)
			assert.NoError(t, err)
			if expectedFloat, ok := tt.expected.(float64); ok {
				if resultFloat, ok := result.(float64); ok {
					assert.InDelta(t, expectedFloat, resultFloat, 0.0001)
					return
				}
			}
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	ctx := &Context{
		Record: map[string]interface{}{
			"Multi_Select__c": "Alpha;Beta;Gamma",
			"Single_Value__c": "Others",
			"Empty_Field__c":  "",
			"Nil_Field__c":    nil,
		},
	}

	tests := []struct {
		name       string
		expression string
		expected   interface{}
	}{
		{
			name:       "value present in multi-select",
			expression: `INCLUDES(Multi_Select__c, "Beta")`,
			expected:   true,
		},
		{
			name:       "value absent from multi-select",
			expression: `INCLUDES(Multi_Select__c, "Delta")`,
			expected:   false,
		},
		{
			name:       "first value in multi-select",
			expression: `INCLUDES(Multi_Select__c, "Alpha")`,
			expected:   true,
		},
		{
			name:       "last value in multi-select",
			expression: `INCLUDES(Multi_Select__c, "Gamma")`,
			expected:   true,
		},
		{
			name:       "exact match not substring - Others vs Other",
			expression: `INCLUDES(Single_Value__c, "Other")`,
			expected:   false,
		},
		{
			name:       "exact match single value",
			expression: `INCLUDES(Single_Value__c, "Others")`,
			expected:   true,
		},
		{
			name:       "empty field returns false",
			expression: `INCLUDES(Empty_Field__c, "Any")`,
			expected:   false,
		},
		{
			name:       "nil field returns false",
			expression: `INCLUDES(Nil_Field__c, "Any")`,
			expected:   false,
		},
		{
			name:       "case sensitive lowercase includes",
			expression: `includes(Multi_Select__c, "Beta")`,
			expected:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(tt.expression, ctx)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	t.Run("ISNEW returns true on insert (no prior)", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{"Name": "Test"},
			Prior:  nil,
		}
		result, err := engine.Evaluate("ISNEW()", ctx)
		assert.NoError(t, err)
		assert.Equal(t, true, result)
	})

	t.Run("ISNEW returns false on update (has prior)", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{"Name": "Test"},
			Prior:  map[string]interface{}{"Name": "Old"},
		}
		result, err := engine.Evaluate("ISNEW()", ctx)
		assert.NoError(t, err)
		assert.Equal(t, false, result)
	})

	t.Run("lowercase isnew normalizes correctly", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{"Name": "Test"},
			Prior:  nil,
		}
		result, err := engine.Evaluate("isnew()", ctx)
		assert.NoError(t, err)
		assert.Equal(t, true, result)
	})

	t.Run("ISNEW in compound expression", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{"StageName": "Prospecting"},
			Prior:  nil,
		}
		result, err := engine.Evaluate(`ISNEW() && StageName != "Prospecting"`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, false, result)
	})
}

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

	t.Run("ISCLONE returns false (not yet implemented)", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{"Name": "Test"},
			Prior:  nil,
		}
		result, err := engine.Evaluate("ISCLONE()", ctx)
		assert.NoError(t, err)
		assert.Equal(t, false, result)
	})

	t.Run("AND with ISCLONE short-circuits to false", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{"StageName": "Closed Won"},
			Prior:  nil,
		}
		result, err := engine.Evaluate(`AND(ISCLONE(), ISNEW(), ISPICKVAL(StageName, "Closed Won"))`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, false, result)
	})

	// Regression: #258 — LEN(null) must not crash when stage is unrelated to the rule.
	// expr-lang eagerly evaluates all AND() arguments before invoking the function,
	// so LEN(null) executes even when ISPICKVAL returns false. The fix is null-safety
	// in LEN() itself (returns 0 for nil, matching Salesforce behavior).
	t.Run("AND with LEN on null field does not crash when stage is not Closed Lost", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{
				"StageName":            "Launched",
				"Loss_Reason_Notes__c": nil,
			},
			Prior: map[string]interface{}{
				"StageName":            "Prospecting",
				"Loss_Reason_Notes__c": nil,
			},
		}
		result, err := engine.Evaluate(
			`AND(ISCHANGED(StageName), ISPICKVAL(StageName, "Closed Lost"), LEN(Loss_Reason_Notes__c) < 15)`,
			ctx,
		)
		assert.NoError(t, err)
		assert.Equal(t, false, result)
	})
}

// TestFormulaEngine_EmptyStringComparisonIsNullSafe locks in the Salesforce
// semantic that `X = ""` on a text field returns TRUE when X is NULL. The
// reported RevRec-style flow formula shape:
//
//	IF({!$Record.Contracting_Entity_EMEA_Only__c}="",
//	   TEXT({!$Record.Account.Contracting_Entity__c}),
//	   {!$Record.Contracting_Entity_EMEA_Only__c})
//
// and EMEA_only__c is stored as NULL on most Orders. Before the fix,
// expr-lang evaluated `nil == ""` as false → IF took the else branch →
// formula returned nil → executeRecordUpdate persisted NULL. Issue #1680.
func TestFormulaEngine_EmptyStringComparisonIsNullSafe(t *testing.T) {
	engine := NewEngine()

	t.Run("X = \"\" returns TRUE when X is nil", func(t *testing.T) {
		ctx := &Context{Record: map[string]interface{}{"Field__c": nil}}
		result, err := engine.Evaluate(`IF(Field__c="", "blank", "not blank")`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, "blank", result)
	})

	t.Run("X = \"\" returns TRUE when X is empty string", func(t *testing.T) {
		ctx := &Context{Record: map[string]interface{}{"Field__c": ""}}
		result, err := engine.Evaluate(`IF(Field__c="", "blank", "not blank")`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, "blank", result)
	})

	t.Run("X = \"\" returns FALSE when X has a value", func(t *testing.T) {
		ctx := &Context{Record: map[string]interface{}{"Field__c": "hello"}}
		result, err := engine.Evaluate(`IF(Field__c="", "blank", "not blank")`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, "not blank", result)
	})

	t.Run("X <> \"\" returns FALSE when X is nil", func(t *testing.T) {
		ctx := &Context{Record: map[string]interface{}{"Field__c": nil}}
		result, err := engine.Evaluate(`IF(Field__c<>"", "not blank", "blank")`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, "blank", result)
	})

	t.Run("X <> \"\" returns TRUE when X has a value", func(t *testing.T) {
		ctx := &Context{Record: map[string]interface{}{"Field__c": "hello"}}
		result, err := engine.Evaluate(`IF(Field__c<>"", "not blank", "blank")`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, "not blank", result)
	})

	// Reported RevRec-style formula shape with merge-field braces
	// and a cross-object TEXT() branch. When EMEA_only is nil, the IF must
	// select the TEXT(Account.X) branch — which was the reported regression.
	t.Run("RevRec-style formula: nil EMEA_only selects Account branch", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{
				"Contracting_Entity_EMEA_Only__c": nil,
				"Account": map[string]interface{}{
					"Contracting_Entity__c": "Example Pte. Ltd.",
				},
			},
		}
		result, err := engine.Evaluate(
			`IF({!$Record.Contracting_Entity_EMEA_Only__c}="",TEXT({!$Record.Account.Contracting_Entity__c}),{!$Record.Contracting_Entity_EMEA_Only__c})`,
			ctx,
		)
		assert.NoError(t, err)
		assert.Equal(t, "Example Pte. Ltd.", result)
	})
}

// TestFormulaEngine_BooleanRewriteDottedPath locks in that the
// `field == true` / `field == false` rewrites wrap the ENTIRE dotted
// identifier path in TRUTHY(...), not just the tail segment. A live
// Nonstandard_Order formula IF(Quote.Contains_Other_Nonstd_Terms__c = True, ...)
// previously normalized to `Quote.TRUTHY(Contains_Other_Nonstd_Terms__c)` —
// expr-lang then blew up looking for a `TRUTHY` method on `Quote`.
func TestFormulaEngine_BooleanRewriteDottedPath(t *testing.T) {
	t.Run("normalizer wraps full dotted path in TRUTHY", func(t *testing.T) {
		out := normalizeFormulaExpression(`Quote.Contains_Other_Nonstd_Terms__c == true`)
		assert.Contains(t, out, "TRUTHY(Quote.Contains_Other_Nonstd_Terms__c)")
		assert.NotContains(t, out, "Quote.TRUTHY(")
	})

	t.Run("normalizer wraps full dotted path in NOT(TRUTHY(...)) for false", func(t *testing.T) {
		out := normalizeFormulaExpression(`Quote.Contains_Other_Nonstd_Terms__c == false`)
		assert.Contains(t, out, "NOT(TRUTHY(Quote.Contains_Other_Nonstd_Terms__c))")
		assert.NotContains(t, out, "Quote.NOT(")
	})

	engine := NewEngine()

	t.Run("Quote.Flag = True evaluates via relationship map", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{
				"Quote": map[string]interface{}{
					"Contains_Other_Nonstd_Terms__c": true,
				},
			},
		}
		result, err := engine.Evaluate(
			`IF(Quote.Contains_Other_Nonstd_Terms__c = True, True, False)`,
			ctx,
		)
		assert.NoError(t, err)
		assert.Equal(t, true, result)
	})

	t.Run("Quote.Flag = True is false when flag is false", func(t *testing.T) {
		ctx := &Context{
			Record: map[string]interface{}{
				"Quote": map[string]interface{}{
					"Contains_Other_Nonstd_Terms__c": false,
				},
			},
		}
		result, err := engine.Evaluate(
			`IF(Quote.Contains_Other_Nonstd_Terms__c = True, True, False)`,
			ctx,
		)
		assert.NoError(t, err)
		assert.Equal(t, false, result)
	})

	t.Run("bare identifier form still rewrites correctly (regression guard)", func(t *testing.T) {
		ctx := &Context{Record: map[string]interface{}{"Allow_Close__c": true}}
		result, err := engine.Evaluate(`IF(Allow_Close__c == True, "yes", "no")`, ctx)
		assert.NoError(t, err)
		assert.Equal(t, "yes", result)
	})
}

// TestEvaluate_TODAY_Plus_30 pins the formula-engine syntax used by the
// Lead Convert CloseDate ActionFieldDefault seed
// (_System_ActionFieldDefault.ValueFormula = "TODAY() + 30").
//
// The engine rewrites TODAY()+N to DATE_ADD(TODAY(), N) (see engine.go:419)
// and returns the result as an ISO-formatted date string ("YYYY-MM-DD"),
// NOT a time.Time. Downstream persistence code that coerces Date field
// inputs must therefore accept that string form — otherwise the shipped
// CloseDate seed will fail type coercion on Opportunity.Insert and produce
// a required-field 400 on convertLead. Gate before seeding.
func TestEvaluate_TODAY_Plus_30(t *testing.T) {
	engine := NewEngine()

	result, err := engine.Evaluate("TODAY() + 30", &Context{})
	require.NoError(t, err, "TODAY() + 30 must parse and evaluate")
	require.NotNil(t, result, "TODAY() + 30 must not return nil")

	got, ok := result.(string)
	require.Truef(t, ok, "expected ISO date string, got %T (%v)", result, result)

	parsed, err := time.Parse("2006-01-02", got)
	require.NoErrorf(t, err, "result %q must parse as YYYY-MM-DD", got)

	expected := time.Now().AddDate(0, 0, 30)
	diff := parsed.Sub(expected)
	if diff < 0 {
		diff = -diff
	}
	// ±2 days covers day-boundary races and clock skew during the test run.
	assert.Lessf(t, diff, 48*time.Hour,
		"TODAY() + 30 = %s, expected ≈ %s (diff %v)",
		got, expected.Format("2006-01-02"), diff)
}

// TestEvaluate_FetcherError_ReturnsErrRelationFetch verifies the engine
// surfaces fetcher errors as ErrRelationFetch (wrapped via %w) instead of
// silently swallowing and running the formula with the relation = nil. This
// is the engine-level half of the issue #2057 fix; the records caller test
// (TestComputeFormulaFields_RepoError_PreservesOldValue) covers the
// downstream "preserve old value" contract.
func TestEvaluate_FetcherError_ReturnsErrRelationFetch(t *testing.T) {
	engine := NewEngine()

	wantErr := errors.New("simulated transient repo failure")
	ctx := &Context{
		Record: map[string]interface{}{
			"AccountId": "001-x",
			// Account relation NOT inline — forces fetcher invocation.
		},
		Fetcher: func(_ map[string]interface{}, rel string) (map[string]interface{}, error) {
			return nil, fmt.Errorf("fetch %q: %w", rel, wantErr)
		},
	}

	// Expression references __Record.Account.Name → forces a relation
	// fetch for "Account" via extractRecordRelations.
	result, err := engine.Evaluate("TEXT($Record.Account.Name)", ctx)

	require.Error(t, err, "fetcher error must propagate (pre-#2057 was swallowed)")
	assert.True(t, errors.Is(err, ErrRelationFetch),
		"propagated error must be classifiable as ErrRelationFetch via errors.Is")
	assert.Nil(t, result, "no usable result on fetch failure")
}

// TestEvaluate_FetcherDefinitiveNull_NotErrRelationFetch verifies the
// definitive-null fetcher contract: a (nil, nil) return is NOT classified
// as ErrRelationFetch. The expression engine may still raise downstream
// (e.g. dereferencing the resulting nil parent for a field access), but
// that error is an evaluation error, not a fetch error — so the records
// caller's "preserve old value" branch must NOT fire for this case.
func TestEvaluate_FetcherDefinitiveNull_NotErrRelationFetch(t *testing.T) {
	engine := NewEngine()

	ctx := &Context{
		Record: map[string]interface{}{
			"AccountId": "",
		},
		Fetcher: func(_ map[string]interface{}, _ string) (map[string]interface{}, error) {
			return nil, nil // definitive null
		},
	}

	_, err := engine.Evaluate("TEXT($Record.Account.Name)", ctx)
	if err != nil {
		// Some expressions raise on nil deref — fine. The point is that
		// the error is NOT classified as a fetcher error.
		assert.False(t, errors.Is(err, ErrRelationFetch),
			"definitive-null (nil, nil) MUST NOT be classified as ErrRelationFetch; got %v", err)
	}
}

// =============================================================================
// Tests for extractBareRelations (issue #2092)
// =============================================================================

func TestExtractBareRelations_AccountDotField(t *testing.T) {
	// The canonical failing case from issue #2092: TEXT(Account.Region__c)
	// should surface "Account" as a bare relation name.
	got := extractBareRelations("TEXT(Account.Region__c)")
	require.Equal(t, []string{"Account"}, got)
}

func TestExtractBareRelations_RecordTypeDotName(t *testing.T) {
	got := extractBareRelations("RecordType.Name")
	require.Equal(t, []string{"RecordType"}, got)
}

func TestExtractBareRelations_MultipleDistinctRelations(t *testing.T) {
	// Two distinct bare relations in one expression — both extracted once.
	got := extractBareRelations("TEXT(Account.Region__c) & RecordType.Name")
	require.Equal(t, []string{"Account", "RecordType"}, got)
}

func TestExtractBareRelations_DeduplicatesRepeatedRelation(t *testing.T) {
	// Same relation used twice — only one entry returned.
	got := extractBareRelations("TEXT(Account.Region__c) & TEXT(Account.Name)")
	require.Equal(t, []string{"Account"}, got)
}

func TestExtractBareRelations_IgnoresNormalisedGlobalVars(t *testing.T) {
	// __Record.X.Y is handled by extractRecordRelations; extractBareRelations
	// must skip __-prefixed identifiers so they are not double-fetched.
	got := extractBareRelations("__Record.Account.Region__c")
	assert.Empty(t, got, "__Record should not appear as a bare relation")
}

func TestExtractBareRelations_NoMatch_BareFlatField(t *testing.T) {
	// Plain field reference (no dot) must not be returned.
	got := extractBareRelations("AccountId")
	assert.Empty(t, got)
}

func TestExtractBareRelations_CustomLookupRelation(t *testing.T) {
	// Custom lookup relationship names with __r suffix must be extracted.
	// These are valid Salesforce bare-relation references (e.g. Account__r.Name).
	got := extractBareRelations("TEXT(Account__r.Name)")
	require.Equal(t, []string{"Account__r"}, got)

	got2 := extractBareRelations("Custom_Lookup__r.Value__c")
	require.Equal(t, []string{"Custom_Lookup__r"}, got2)
}

func TestExtractBareRelations_NoMatch_StringLiteralContent(t *testing.T) {
	// Identifier inside a string literal must not be extracted.
	got := extractBareRelations(`IF(Name == "Account.Name", "yes", "no")`)
	// "Account" inside the double-quoted string is preceded by ", which is
	// excluded from the boundary class — must NOT be extracted.
	assert.NotContains(t, got, "Account")
}

func TestExtractBareRelations_NoMatch_StringLiteralContent_ProductionPath(t *testing.T) {
	// extractBareRelations strips string literals internally before scanning,
	// so dotted identifiers inside quoted strings never produce spurious
	// relation references — regardless of whether the opening quote immediately
	// precedes the identifier ("Account.Name") or a space does ("See Account.Name").
	cases := []string{
		`IF(Name == "Account.Name", "yes", "no")`,     // immediate quote
		`IF(Name == "See Account.Name", "yes", "no")`, // space before ident
		`IF(Name == 'Account.Name', 'yes', 'no')`,     // single quotes
		`IF(Name == 'See Account.Name', 'yes', 'no')`, // single quotes + space
		`TEXT("prefix Account.Region__c suffix")`,     // longer string
	}
	for _, raw := range cases {
		got := extractBareRelations(raw)
		assert.NotContains(t, got, "Account",
			"extractBareRelations(%q) should not extract 'Account' from string literal content", raw)
		// Also verify the production code path (through normalizeFormulaExpression) is safe.
		got2 := extractBareRelations(normalizeFormulaExpression(raw))
		assert.NotContains(t, got2, "Account",
			"extractBareRelations(normalizeFormulaExpression(%q)) should not extract 'Account'", raw)
	}
}

// =============================================================================
// Evaluate with bare X.Y cross-object refs (issue #2092)
// =============================================================================

// TestEvaluate_BareRelation_AccountDotField verifies that a formula using the
// Salesforce implicit-record-context shorthand Account.Region__c (without
// $Record. prefix) correctly fetches the parent Account and resolves the field.
// This is the primary acceptance test for issue #2092.
func TestEvaluate_BareRelation_AccountDotField(t *testing.T) {
	engine := NewEngine()

	ctx := &Context{
		Record: map[string]interface{}{
			"AccountId": "acc-1",
			// Account NOT in record — fetcher must supply it.
		},
		Fetcher: func(_ map[string]interface{}, rel string) (map[string]interface{}, error) {
			if rel == "Account" {
				return map[string]interface{}{"Id": "acc-1", "Region__c": "Japan"}, nil
			}
			return nil, nil
		},
	}

	result, err := engine.Evaluate("TEXT(Account.Region__c)", ctx)

	require.NoError(t, err, "bare Account.Region__c must resolve without error")
	assert.Equal(t, "Japan", result)
}

// TestEvaluate_BareRelation_RecordTypeDotName verifies RecordType.Name resolves
// via the implicit-record-context shorthand.
func TestEvaluate_BareRelation_RecordTypeDotName(t *testing.T) {
	engine := NewEngine()

	ctx := &Context{
		Record: map[string]interface{}{
			"RecordTypeId": "rt-1",
		},
		Fetcher: func(_ map[string]interface{}, rel string) (map[string]interface{}, error) {
			if rel == "RecordType" {
				return map[string]interface{}{"Id": "rt-1", "Name": "Enterprise"}, nil
			}
			return nil, nil
		},
	}

	result, err := engine.Evaluate("RecordType.Name", ctx)

	require.NoError(t, err)
	assert.Equal(t, "Enterprise", result)
}

// TestEvaluate_BareRelation_FetcherError_SurfacesErrRelationFetch verifies that
// a transient fetcher error on a bare relation is classified as ErrRelationFetch,
// consistent with the $Record.X.Y path contract from issue #2057.
func TestEvaluate_BareRelation_FetcherError_SurfacesErrRelationFetch(t *testing.T) {
	engine := NewEngine()

	ctx := &Context{
		Record: map[string]interface{}{"AccountId": "acc-x"},
		Fetcher: func(_ map[string]interface{}, rel string) (map[string]interface{}, error) {
			return nil, fmt.Errorf("transient: %w", errors.New("db blip"))
		},
	}

	_, err := engine.Evaluate("TEXT(Account.Region__c)", ctx)
	require.Error(t, err)
	assert.True(t, errors.Is(err, ErrRelationFetch),
		"transient fetcher error on bare relation must surface as ErrRelationFetch")
}

// TestEvaluate_BareRelation_InlineAlreadyPopulated verifies that a bare X.Y
// relation that is ALREADY inline in the record (pre-populated by the caller)
// is used directly without a fetcher call — augmentRecordWithRelations must
// be idempotent.
func TestEvaluate_BareRelation_InlineAlreadyPopulated(t *testing.T) {
	engine := NewEngine()
	fetchCalled := false

	ctx := &Context{
		Record: map[string]interface{}{
			"AccountId": "acc-1",
			"Account": map[string]interface{}{ // already inline
				"Region__c": "EMEA",
			},
		},
		Fetcher: func(_ map[string]interface{}, rel string) (map[string]interface{}, error) {
			fetchCalled = true
			return map[string]interface{}{"Region__c": "WRONG"}, nil
		},
	}

	result, err := engine.Evaluate("TEXT(Account.Region__c)", ctx)
	require.NoError(t, err)
	assert.Equal(t, "EMEA", result, "inline value must be used")
	assert.False(t, fetchCalled, "fetcher must not be called when relation is already inline")
}

// TestFormulaEngine_Depth1_DefinitiveNullRelation is the regression test for
// issue #2181 r2: when Fetcher returns (nil, nil) for a bare depth-1 relation
// reference (e.g. Account.Name, RecordType.DeveloperName), the formula must
// evaluate to the null-propagated result rather than failing with
// "unknown name Account" or "type nil has no field DeveloperName".
func TestFormulaEngine_Depth1_DefinitiveNullRelation(t *testing.T) {
	engine := NewEngine()
	nullFetcher := func(_ map[string]interface{}, _ string) (map[string]interface{}, error) {
		return nil, nil // always definitive null
	}
	ctx := &Context{
		Record:  map[string]interface{}{"Name": "Test Record"},
		Fetcher: nullFetcher,
	}

	// TEXT(Account.Name) with FK cleared / orphan FK (fetcher returns nil,nil) → "".
	result, err := engine.Evaluate("TEXT(Account.Name)", ctx)
	require.NoError(t, err, "TEXT(Account.Name) with definitive-null Account must not error")
	assert.Equal(t, "", result)

	// IF(RecordType.DeveloperName='New_Subscription','Yes','No') with RecordType=nil → "No".
	result2, err2 := engine.Evaluate("IF(RecordType.DeveloperName='New_Subscription','Yes','No')", ctx)
	require.NoError(t, err2, "IF with null RecordType must not error")
	assert.Equal(t, "No", result2)
}

// TestEvaluate_ReservedRoot_RecordFieldNotClobbered is the regression test for
// the R3 bug: extractBareRelations was treating "record" in "record.Amount" as a
// bare relation, causing augmentRecordWithRelations to install record:{} into the
// augmented map; the subsequent flattening loop then overwrote env["record"] with
// that empty map, making every record.<field> formula return nil when Fetcher was
// set. Issue #2181 r3.
func TestEvaluate_ReservedRoot_RecordFieldNotClobbered(t *testing.T) {
	engine := NewEngine()

	ctx := &Context{
		Record: map[string]interface{}{
			"Amount": float64(42),
		},
		// Fetcher returns definitive null for any unknown relation.
		// Before the fix, "record" was passed here, (nil,nil) was returned,
		// and record:{} was injected — clobbering env["record"].
		Fetcher: func(_ map[string]interface{}, _ string) (map[string]interface{}, error) {
			return nil, nil
		},
	}

	result, err := engine.Evaluate("record.Amount * 2", ctx)
	require.NoError(t, err, "record.Amount with Fetcher set must not error")
	assert.Equal(t, float64(84), result, "record.Amount must resolve to 42, not clobbered by empty-map augmentation")
}

// TestExtractBareRelations_SkipsReservedRoots verifies that the five reserved
// evaluation roots (record, prior, user, env, var) are not returned by
// extractBareRelations even when they appear as dotted prefixes.
func TestExtractBareRelations_SkipsReservedRoots(t *testing.T) {
	reserved := []string{"record", "prior", "user", "env", "var"}
	for _, root := range reserved {
		expr := root + ".SomeField"
		got := extractBareRelations(expr)
		assert.NotContains(t, got, root,
			"extractBareRelations(%q) must not extract reserved root %q", expr, root)
	}
}

// TestEvaluate_UserScopeFlatAliases_DoNotShadowRecordFields locks in the fix
// for #2260's IANTHEREAL P1 blocker: BuildUserFormulaScope keeps flat
// aliases (Id/Name/Email/ProfileId) inside ctx.User for ActionService's
// env["user"].Email merge path, but they MUST NOT shadow record fields with
// the same name when the engine flattens ctx.User into the top-level env.
//
// Before the fix, a validation rule or flow formula on a record where
// record.Name="RecordName" and the running user has Name="UserName" would
// silently evaluate bare `Name` to "UserName" instead of "RecordName" —
// exactly the kind of silent data-confusion that lets invalid records pass
// validation or blocks valid ones (IANTHEREAL #2260 review-4195221362).
//
// Fix: engine.Evaluate's ctx.User flatten loop now promotes only normalized
// global pseudo-keys (__Profile, __User, __UserRole) to the top-level env,
// not the flat aliases. Bare `Name` therefore continues to resolve to the
// record's Name; user.Name remains accessible via env["user"].Name and the
// __User.Name sub-entity.
func TestEvaluate_UserScopeFlatAliases_DoNotShadowRecordFields(t *testing.T) {
	engine := NewEngine()

	ctx := &Context{
		Record: map[string]interface{}{
			"Id":    "rec-001",
			"Name":  "RecordName",
			"Email": "record@example.com",
		},
		User: map[string]interface{}{
			// Flat aliases — kept inside ctx.User for ActionService's
			// env["user"].Email merge access. These must NOT promote to
			// top-level env or they'd shadow the record's same-named fields.
			"Id":        "user-001",
			"Name":      "UserName",
			"Email":     "user@example.com",
			"ProfileId": "profile_admin",
			// SF global pseudo-entities (post $-→__-normalisation). These DO
			// promote to top-level so $User.Name etc. resolve correctly.
			"__User": map[string]interface{}{
				"Id":    "user-001",
				"Name":  "UserName",
				"Email": "user@example.com",
			},
		},
	}

	tests := []struct {
		name string
		expr string
		want interface{}
	}{
		{"bare Name resolves to record value", "Name", "RecordName"},
		{"bare Id resolves to record value", "Id", "rec-001"},
		{"bare Email resolves to record value", "Email", "record@example.com"},
		{"record.Name still resolves to record value", "record.Name", "RecordName"},
		{"user.Name resolves to user value", "user.Name", "UserName"},
		{"user.Email resolves to user value", "user.Email", "user@example.com"},
		{"__User.Name resolves to user value", "__User.Name", "UserName"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := engine.Evaluate(tt.expr, ctx)
			require.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}
