package automation

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// isBlankValue helper — Salesforce ISBLANK semantics
// ---------------------------------------------------------------------------
//
// Salesforce ISBLANK(x) returns true when:
//   - x is null
//   - x is an empty string ""
//   - x is an empty collection
//
// Whitespace-only strings are NOT blank (SF parity — matches this repo's
// expression engine; see pkg/expression/unicode_test.go "spaces are NOT blank").
//
// Critically, ISBLANK(0) is FALSE for numeric fields — zero is a value, not blank.
// This is the key differentiator from JS falsy / generic "empty" checks.
//
// Typed-value wrappers from Salesforce JSON (e.g. {"stringValue": ""}) are
// unwrapped before the check, so flow variable defaults stored as wrappers
// evaluate correctly.
//
// Reference: Salesforce Formulas docs, ISBLANK / ISNOTBLANK operators.
// Used by flows such as Contract_Update_Account_Contract_Valid.

func TestIsBlankValue(t *testing.T) {
	tests := []struct {
		name  string
		value interface{}
		want  bool
	}{
		// --- blank cases ---
		{"nil", nil, true},
		{"empty string", "", true},
		{"empty []interface{}", []interface{}{}, true},
		{"empty []string", []string{}, true},
		{"empty []models.SObject (RecordLookup with no rows)", []models.SObject{}, true},
		{"nil []models.SObject", []models.SObject(nil), true},
		{"empty map[string]interface{}", map[string]interface{}{}, true},
		{"empty SObject (map)", models.SObject{}, true},

		// Typed-value wrappers (from SF JSON) — unwrapped via extractTypedValue
		// before the check.
		{"typed wrapper with empty stringValue", map[string]interface{}{"stringValue": ""}, true},
		{"typed wrapper with non-empty stringValue", map[string]interface{}{"stringValue": "hi"}, false},

		// All-nil typed wrapper — SF metadata serializes "no value provided"
		// as the full wide envelope with every arm null. Must read as blank.
		{
			"all-nil typed wrapper (SF 'no value' serialization)",
			map[string]interface{}{
				"apexValue": nil, "booleanValue": nil, "complexValue": nil,
				"dateTimeValue": nil, "dateValue": nil, "elementReference": nil,
				"numberValue": nil, "sobjectValue": nil, "stringValue": nil,
			},
			true,
		},
		{
			"typed wrapper with any populated arm is NOT blank",
			map[string]interface{}{
				"apexValue": nil, "booleanValue": nil, "stringValue": "hi",
				"elementReference": nil,
			},
			false,
		},
		{
			"typed wrapper with populated elementReference",
			map[string]interface{}{
				"stringValue": nil, "elementReference": "$Record.Name",
			},
			false,
		},
		{
			// Non-typed map with a single nil entry must NOT be misclassified
			// as a typed wrapper.
			"non-typed map with one nil entry is NOT blank",
			map[string]interface{}{"customField": nil},
			false,
		},

		// --- non-blank cases ---
		// Whitespace-only strings are NOT blank (SF parity).
		{"whitespace-only string is NOT blank (SF parity)", "   ", false},
		{"tab-only string is NOT blank", "\t", false},
		{"non-empty string", "hello", false},
		{"string with leading/trailing space and content", "  hello  ", false},
		// Zero-value guards — the critical SF differentiator.
		{"int zero is NOT blank", 0, false},
		{"int64 zero is NOT blank", int64(0), false},
		{"float zero is NOT blank", float64(0), false},
		{"int non-zero", 42, false},
		{"float non-zero", 3.14, false},
		{"bool false is NOT blank", false, false},
		{"bool true is NOT blank", true, false},
		{"non-empty slice", []interface{}{"a"}, false},
		{"non-empty []string", []string{"a"}, false},
		{"non-empty []models.SObject", []models.SObject{{"Id": "a"}}, false},
		{"non-empty map", map[string]interface{}{"k": "v"}, false},
		{"non-empty SObject", models.SObject{"Id": "a"}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, isBlankValue(tt.value))
		})
	}
}

// ---------------------------------------------------------------------------
// evaluateCondition — IsBlank / IsNotBlank operators
// ---------------------------------------------------------------------------

func TestEvaluateCondition_IsBlank(t *testing.T) {
	executor := newTestExecutor()

	tests := []struct {
		name     string
		context  map[string]interface{}
		cond     models.FlowCondition
		expected bool
	}{
		// --- IsBlank true cases ---
		{
			name:     "IsBlank - nil value in context",
			context:  map[string]interface{}{"val": nil},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank - empty string",
			context:  map[string]interface{}{"val": ""},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank"},
			expected: true,
		},
		{
			// SF parity: whitespace-only strings are NOT blank. Matches the
			// repo's expression engine (pkg/expression/unicode_test.go:60).
			name:     "IsBlank - whitespace only is NOT blank",
			context:  map[string]interface{}{"val": "   "},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - nested ref to missing field resolves to nil",
			context:  map[string]interface{}{"$Record": models.SObject{"Id": "x"}},
			cond:     models.FlowCondition{LeftValueReference: "$Record.AbsentField__c", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank - empty slice",
			context:  map[string]interface{}{"items": []interface{}{}},
			cond:     models.FlowCondition{LeftValueReference: "items", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank - empty map",
			context:  map[string]interface{}{"obj": map[string]interface{}{}},
			cond:     models.FlowCondition{LeftValueReference: "obj", Operator: "IsBlank"},
			expected: true,
		},

		// --- IsBlank false cases ---
		{
			name:     "IsBlank - non-empty string",
			context:  map[string]interface{}{"val": "hello"},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - zero int is NOT blank (SF semantics)",
			context:  map[string]interface{}{"amount": 0},
			cond:     models.FlowCondition{LeftValueReference: "amount", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - zero float is NOT blank",
			context:  map[string]interface{}{"amount": float64(0)},
			cond:     models.FlowCondition{LeftValueReference: "amount", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - non-zero number",
			context:  map[string]interface{}{"amount": float64(42)},
			cond:     models.FlowCondition{LeftValueReference: "amount", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - false bool is NOT blank",
			context:  map[string]interface{}{"flag": false},
			cond:     models.FlowCondition{LeftValueReference: "flag", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - non-empty slice",
			context:  map[string]interface{}{"items": []interface{}{"a"}},
			cond:     models.FlowCondition{LeftValueReference: "items", Operator: "IsBlank"},
			expected: false,
		},

		// --- IsNotBlank (symmetric inverse) ---
		{
			name:     "IsNotBlank - nil is blank, so IsNotBlank false",
			context:  map[string]interface{}{"val": nil},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsNotBlank"},
			expected: false,
		},
		{
			name:     "IsNotBlank - empty string false",
			context:  map[string]interface{}{"val": ""},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsNotBlank"},
			expected: false,
		},
		{
			// SF parity: whitespace is NOT blank, so IsNotBlank is true.
			name:     "IsNotBlank - whitespace is true (SF parity)",
			context:  map[string]interface{}{"val": "  "},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsNotBlank"},
			expected: true,
		},
		{
			name:     "IsNotBlank - non-empty string true",
			context:  map[string]interface{}{"val": "hello"},
			cond:     models.FlowCondition{LeftValueReference: "val", Operator: "IsNotBlank"},
			expected: true,
		},
		{
			name:     "IsNotBlank - zero int true (SF semantics)",
			context:  map[string]interface{}{"amount": 0},
			cond:     models.FlowCondition{LeftValueReference: "amount", Operator: "IsNotBlank"},
			expected: true,
		},
		{
			name:     "IsNotBlank - nested ref to missing field is blank",
			context:  map[string]interface{}{"$Record": models.SObject{"Id": "x"}},
			cond:     models.FlowCondition{LeftValueReference: "$Record.AbsentField__c", Operator: "IsNotBlank"},
			expected: false,
		},

		// --- IsBlank with rightValue=false (SF flow XML pattern) ---
		// Flow XML: operator=IsBlank rightValue=false means "IS NOT blank".
		// This is the pattern used by lead_convert_opportunity and other flows.
		{
			name:    "IsBlank rightValue=false - non-empty field returns true (NOT blank)",
			context: map[string]interface{}{"val": "some value"},
			cond:    models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank", RightValue: false},
			expected: true,
		},
		{
			name:    "IsBlank rightValue=false - empty field returns false (IS blank, condition fails)",
			context: map[string]interface{}{"val": ""},
			cond:    models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank", RightValue: false},
			expected: false,
		},
		{
			name:    "IsBlank rightValue=false - nil field returns false",
			context: map[string]interface{}{"val": nil},
			cond:    models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank", RightValue: false},
			expected: false,
		},
		{
			name:    "IsBlank rightValue=true - empty field returns true (IS blank)",
			context: map[string]interface{}{"val": ""},
			cond:    models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank", RightValue: true},
			expected: true,
		},
		{
			name:    "IsBlank rightValue=true - non-empty field returns false (NOT blank)",
			context: map[string]interface{}{"val": "hello"},
			cond:    models.FlowCondition{LeftValueReference: "val", Operator: "IsBlank", RightValue: true},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := executor.evaluateCondition(tt.context, &tt.cond)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// ---------------------------------------------------------------------------
// evaluateRecordFilter — IsBlank / IsNotBlank operators
// ---------------------------------------------------------------------------

func TestEvaluateRecordFilter_IsBlank(t *testing.T) {
	executor := newTestExecutor()

	tests := []struct {
		name     string
		record   models.SObject
		filter   models.FlowRecordFilter
		expected bool
	}{
		{
			name:     "IsBlank - missing field key",
			record:   models.SObject{},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank - nil value",
			record:   models.SObject{"Notes": nil},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank - empty string",
			record:   models.SObject{"Notes": ""},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsBlank"},
			expected: true,
		},
		{
			// SF parity — whitespace-only strings are NOT blank.
			name:     "IsBlank - whitespace only is NOT blank (SF parity)",
			record:   models.SObject{"Notes": "   "},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - zero amount is NOT blank (SF semantics)",
			record:   models.SObject{"Amount": float64(0)},
			filter:   models.FlowRecordFilter{Field: "Amount", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - non-empty string",
			record:   models.SObject{"Notes": "data"},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsNotBlank - missing field key",
			record:   models.SObject{},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsNotBlank"},
			expected: false,
		},
		{
			name:     "IsNotBlank - empty string",
			record:   models.SObject{"Notes": ""},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsNotBlank"},
			expected: false,
		},
		{
			name:     "IsNotBlank - zero amount is NOT blank",
			record:   models.SObject{"Amount": float64(0)},
			filter:   models.FlowRecordFilter{Field: "Amount", Operator: "IsNotBlank"},
			expected: true,
		},
		{
			name:     "IsNotBlank - non-empty string",
			record:   models.SObject{"Notes": "data"},
			filter:   models.FlowRecordFilter{Field: "Notes", Operator: "IsNotBlank"},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := executor.evaluateRecordFilter(tt.record, nil, &tt.filter)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// ---------------------------------------------------------------------------
// evaluateConditionCtx — IsBlank mirrors evaluateCondition semantics
// ---------------------------------------------------------------------------

func TestEvaluateConditionCtx_IsBlank(t *testing.T) {
	executor := newTestExecutor()

	// ContextData-based tests (no __r resolution required).
	interview := &models.FlowInterview{
		ContextData: map[string]interface{}{
			"$Record": models.SObject{
				"Id":           "a-1",
				"Name":         "Acme",
				"Notes__c":     "",
				"Amount__c":    float64(0),
				"HasValue__c":  "yes",
				"WhitespaceField__c": "   ",
			},
		},
	}

	cases := []struct {
		name     string
		cond     models.FlowCondition
		expected bool
	}{
		{
			name:     "IsBlank - empty-string field",
			cond:     models.FlowCondition{LeftValueReference: "$Record.Notes__c", Operator: "IsBlank"},
			expected: true,
		},
		{
			// SF parity — whitespace-only strings are NOT blank.
			name:     "IsBlank - whitespace field is NOT blank (SF parity)",
			cond:     models.FlowCondition{LeftValueReference: "$Record.WhitespaceField__c", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - missing field",
			cond:     models.FlowCondition{LeftValueReference: "$Record.MissingField__c", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank - zero amount is NOT blank",
			cond:     models.FlowCondition{LeftValueReference: "$Record.Amount__c", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsBlank - non-empty string",
			cond:     models.FlowCondition{LeftValueReference: "$Record.HasValue__c", Operator: "IsBlank"},
			expected: false,
		},
		{
			name:     "IsNotBlank - non-empty",
			cond:     models.FlowCondition{LeftValueReference: "$Record.HasValue__c", Operator: "IsNotBlank"},
			expected: true,
		},
		{
			name:     "IsNotBlank - empty",
			cond:     models.FlowCondition{LeftValueReference: "$Record.Notes__c", Operator: "IsNotBlank"},
			expected: false,
		},
		{
			name:     "IsNotBlank - zero amount true (SF semantics)",
			cond:     models.FlowCondition{LeftValueReference: "$Record.Amount__c", Operator: "IsNotBlank"},
			expected: true,
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			got := executor.evaluateConditionCtx(context.Background(), interview, nil, &tt.cond)
			assert.Equal(t, tt.expected, got)
		})
	}
}

// ---------------------------------------------------------------------------
// buildScheduledStartFilterClause — emits ISBLANK(...) / NOT(ISBLANK(...))
// ---------------------------------------------------------------------------

func TestBuildScheduledStartFilterClause_IsBlank(t *testing.T) {
	executor := newTestExecutor()

	assert.Equal(t, "ISBLANK(Notes)", executor.buildScheduledStartFilterClause(nil, models.FlowRecordFilter{
		Field:    "Notes",
		Operator: "IsBlank",
	}))
	assert.Equal(t, "NOT(ISBLANK(Notes))", executor.buildScheduledStartFilterClause(nil, models.FlowRecordFilter{
		Field:    "Notes",
		Operator: "IsNotBlank",
	}))
}

// ---------------------------------------------------------------------------
// Full-flow integration: Decision node with IsBlank outcome routes correctly
// ---------------------------------------------------------------------------
//
// This exercises the complete execution path: evaluateConditions -> decision
// outcome selection based on an IsBlank operator. Mirrors the real-world
// pattern from Contract_Update_Account_Contract_Valid, where a decision
// branches on whether an Account relationship field is populated.

// TestContractUpdateAccountContractValid_RealFixture exercises IsBlank against
// the real Flow metadata for Contract_Update_Account_Contract_Valid, dumped
// from the flow_test TiDB database.
//
// The decision rule is:
//
//	decisions[0].rules[0]: leftValueReference="GetContract", operator="IsBlank",
//	  rightValue.booleanValue=false → "GetContract is NOT blank" (a contract was found)
//	  → route to "update_account" to copy Contract_Number__c / EndDate / Contract_Valid__c
//	    from the found contract onto the Account.
//
// This test parses the fixture, wires the RecordLookup result into context,
// and verifies both branches (populated lookup → rule fires; empty lookup →
// rule does not fire, falls to defaultConnector).
func TestContractUpdateAccountContractValid_RealFixture(t *testing.T) {
	path := filepath.Join("testdata", "contract_update_account_contract_valid.json")
	raw, err := os.ReadFile(path)
	require.NoError(t, err, "fixture file must exist")

	var meta models.FlowMetadata
	require.NoError(t, json.Unmarshal(raw, &meta), "fixture must parse")

	require.NotEmpty(t, meta.Decisions, "flow must have decisions")
	decision := meta.Decisions[0]
	require.Equal(t, "if_have_contract", decision.Name)
	require.NotEmpty(t, decision.Rules)
	rule := decision.Rules[0]
	require.Equal(t, "have_contract", rule.Name)
	require.NotEmpty(t, rule.Conditions)

	cond := rule.Conditions[0]
	require.Equal(t, "GetContract", cond.LeftValueReference)
	require.Equal(t, "IsBlank", cond.Operator,
		"fixture anchors the GAP 7 contract — do not mutate")

	executor := newTestExecutor()

	// Branch A: GetContract returned a record (contract found).
	// rightValue.booleanValue=false means "IsBlank == false" → field is NOT blank.
	// Condition fires → rule matches → route to update_account.
	populatedLookup := map[string]interface{}{
		"GetContract": models.SObject{
			"Id":        "800xx000000001",
			"AccountId": "001xx000000001",
			"Status":    "Activated",
		},
	}
	require.True(t,
		executor.evaluateConditions(populatedLookup, rule.Conditions, rule.ConditionLogic),
		"populated GetContract should satisfy IsBlank=false (contract found → update_account)")

	// Branch B: GetContract returned no records.
	// IsBlank(nil) is true, so "IsBlank == false" is false → rule doesn't fire → defaultConnector.
	emptyLookup := map[string]interface{}{
		"GetContract": nil,
	}
	require.False(t,
		executor.evaluateConditions(emptyLookup, rule.Conditions, rule.ConditionLogic),
		"nil GetContract should NOT satisfy IsBlank=false (no contract → skip update)")

	emptyCollectionLookup := map[string]interface{}{
		"GetContract": []interface{}{},
	}
	require.False(t,
		executor.evaluateConditions(emptyCollectionLookup, rule.Conditions, rule.ConditionLogic),
		"empty collection GetContract should NOT satisfy IsBlank=false")

	emptyTypedLookup := map[string]interface{}{
		"GetContract": []models.SObject{},
	}
	require.False(t,
		executor.evaluateConditions(emptyTypedLookup, rule.Conditions, rule.ConditionLogic),
		"empty []models.SObject GetContract should NOT satisfy IsBlank=false")

	// Sanity: the flow's connector target for the rule should be the
	// update_account node — if this ever changes, the fixture was re-dumped
	// with a different flow structure and the test needs to be re-anchored.
	require.NotNil(t, rule.Connector)
	require.Equal(t, "update_account", rule.Connector.TargetReference)
}

// ---------------------------------------------------------------------------
// evaluateRecordFilter — IsBlank rightValue negation (#1780)
// ---------------------------------------------------------------------------
//
// This exercises the Start element entry-condition filter path
// (evaluateRecordFilterForFieldCtx), which previously ignored the rightValue
// for IsBlank. The pattern "IsBlank with rightValue=false" means "field is NOT
// blank" — it is used in flow entry conditions to gate on populated fields.
// Regression test for PR fix (#1780).

func TestEvaluateRecordFilter_IsBlank_RightValueNegation(t *testing.T) {
	executor := newTestExecutor()

	tests := []struct {
		name     string
		record   models.SObject
		filter   models.FlowRecordFilter
		expected bool
	}{
		// rightValue=false means "IsBlank == false" → field is NOT blank.
		{
			name:   "IsBlank rightValue=false - non-empty field returns true (NOT blank → condition passes)",
			record: models.SObject{"Field1": "some value"},
			filter: models.FlowRecordFilter{
				Field:    "Field1",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": false},
			},
			expected: true,
		},
		{
			name:   "IsBlank rightValue=false - empty string returns false (IS blank → condition fails)",
			record: models.SObject{"Field1": ""},
			filter: models.FlowRecordFilter{
				Field:    "Field1",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": false},
			},
			expected: false,
		},
		{
			name:   "IsBlank rightValue=false - nil returns false",
			record: models.SObject{},
			filter: models.FlowRecordFilter{
				Field:    "Field1",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": false},
			},
			expected: false,
		},
		// rightValue=true (or absent) means "IsBlank == true" → standard check.
		{
			name:   "IsBlank rightValue=true - empty field returns true",
			record: models.SObject{"Field1": ""},
			filter: models.FlowRecordFilter{
				Field:    "Field1",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": true},
			},
			expected: true,
		},
		{
			name:   "IsBlank rightValue=true - non-empty field returns false",
			record: models.SObject{"Field1": "hello"},
			filter: models.FlowRecordFilter{
				Field:    "Field1",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": true},
			},
			expected: false,
		},
		// No rightValue (nil) — baseline IsBlank behaviour preserved.
		{
			name:     "IsBlank no rightValue - empty field returns true",
			record:   models.SObject{"Field1": ""},
			filter:   models.FlowRecordFilter{Field: "Field1", Operator: "IsBlank"},
			expected: true,
		},
		{
			name:     "IsBlank no rightValue - non-empty field returns false",
			record:   models.SObject{"Field1": "data"},
			filter:   models.FlowRecordFilter{Field: "Field1", Operator: "IsBlank"},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := executor.evaluateRecordFilter(tt.record, nil, &tt.filter)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// ---------------------------------------------------------------------------
// evaluateRecordFilterForFieldCtx — direct pin test for IsBlank rightValue (#1780)
// ---------------------------------------------------------------------------
//
// HIGH-1 regression guard: the shim test above exercises the legacy path
// (evaluateRecordFilter → evaluateRecordFilterForField → evaluateRecordFilterForFieldCtx).
// This test directly calls evaluateRecordFilterForFieldCtx so that any future
// refactor that breaks the rightValue negation path is caught immediately,
// without relying on the call chain being preserved.
func TestEvaluateRecordFilterForFieldCtx_IsBlank_RightValueNegation(t *testing.T) {
	executor := newTestExecutor()
	ctx := context.Background()

	tests := []struct {
		name     string
		record   models.SObject
		filter   models.FlowRecordFilter
		expected bool
	}{
		// case 1: IsBlank, rightValue=true (explicit), field blank → match (true)
		{
			name:   "IsBlank rightValue=true - blank field matches",
			record: models.SObject{"F": ""},
			filter: models.FlowRecordFilter{
				Field:    "F",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": true},
			},
			expected: true,
		},
		// case 2: IsBlank, rightValue=false, field blank → NO match (false, negated)
		{
			name:   "IsBlank rightValue=false - blank field does NOT match (negated)",
			record: models.SObject{"F": ""},
			filter: models.FlowRecordFilter{
				Field:    "F",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": false},
			},
			expected: false,
		},
		// case 3: IsBlank, rightValue=false, field non-blank → match (true, negated)
		{
			name:   "IsBlank rightValue=false - non-blank field matches (negated)",
			record: models.SObject{"F": "hello"},
			filter: models.FlowRecordFilter{
				Field:    "F",
				Operator: "IsBlank",
				Value:    map[string]interface{}{"booleanValue": false},
			},
			expected: true,
		},
		// case 4: IsBlank, rightValue=nil (no Value), field blank → match (true, default)
		{
			name:     "IsBlank no rightValue - blank field matches (default behavior)",
			record:   models.SObject{"F": ""},
			filter:   models.FlowRecordFilter{Field: "F", Operator: "IsBlank"},
			expected: true,
		},
		// case 5: IsBlank no rightValue - non-blank field → no match (default behavior)
		{
			name:     "IsBlank no rightValue - non-blank field does NOT match (default)",
			record:   models.SObject{"F": "data"},
			filter:   models.FlowRecordFilter{Field: "F", Operator: "IsBlank"},
			expected: false,
		},
		// IsNotBlank baseline (no rightValue negation - IsNotBlank is its own inverse)
		// case 6: IsNotBlank, no rightValue, blank field → false
		{
			name:     "IsNotBlank no rightValue - blank field does NOT match",
			record:   models.SObject{"F": ""},
			filter:   models.FlowRecordFilter{Field: "F", Operator: "IsNotBlank"},
			expected: false,
		},
		// case 7: IsNotBlank, no rightValue, non-blank field → true
		{
			name:     "IsNotBlank no rightValue - non-blank field matches",
			record:   models.SObject{"F": "hello"},
			filter:   models.FlowRecordFilter{Field: "F", Operator: "IsNotBlank"},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := executor.evaluateRecordFilterForFieldCtx(ctx, tt.record, nil, &tt.filter, nil)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestEvaluateConditions_IsBlankDecisionRouting(t *testing.T) {
	executor := newTestExecutor()

	// Scenario: "Contract has blank Account" is the entry condition; a second
	// "Account not blank" condition should route to the opposite branch.
	blankAccount := map[string]interface{}{
		"$Record": models.SObject{"Id": "c-1", "AccountId": ""},
	}
	populated := map[string]interface{}{
		"$Record": models.SObject{"Id": "c-2", "AccountId": "001xx"},
	}

	isBlankCond := []models.FlowCondition{
		{LeftValueReference: "$Record.AccountId", Operator: "IsBlank"},
	}
	isNotBlankCond := []models.FlowCondition{
		{LeftValueReference: "$Record.AccountId", Operator: "IsNotBlank"},
	}

	assert.True(t, executor.evaluateConditions(blankAccount, isBlankCond, "and"),
		"blank AccountId should satisfy IsBlank")
	assert.False(t, executor.evaluateConditions(blankAccount, isNotBlankCond, "and"),
		"blank AccountId should NOT satisfy IsNotBlank")
	assert.False(t, executor.evaluateConditions(populated, isBlankCond, "and"),
		"populated AccountId should NOT satisfy IsBlank")
	assert.True(t, executor.evaluateConditions(populated, isNotBlankCond, "and"),
		"populated AccountId should satisfy IsNotBlank")
}
