package formula

// concat_null_coerce_test.go — parity tests for Issue #468.
//
// Salesforce v63 oracle: text concatenation (`&` and `+` with string operands)
// coerces a NULL operand to the empty string. Numeric `+` propagates NULL.

import (
	"testing"
)

// TestTextConcatNullCoerce verifies that the formula engine follows Salesforce
// v63 semantics for null coercion in text concatenation operators.
func TestTextConcatNullCoerce(t *testing.T) {
	engine := NewEngine()

	tests := []struct {
		name   string
		expr   string
		record map[string]interface{}
		want   interface{}
	}{
		// --- & operator (text concat) ---

		// SF oracle: "Solo" & null → "Solo"
		{
			name:   "amp_right_null",
			expr:   `{!$Record.LastName}&{!$Record.FirstName}`,
			record: map[string]interface{}{"LastName": "Solo", "FirstName": nil},
			want:   "Solo",
		},
		// SF oracle: null & "Han" → "Han"
		{
			name:   "amp_left_null",
			expr:   `{!$Record.FirstName}&{!$Record.LastName}`,
			record: map[string]interface{}{"LastName": "Han", "FirstName": nil},
			want:   "Han",
		},
		// Both null: conservative nil (see ssot-invariants.md Known Acceptable Limitation)
		{
			name:   "amp_both_null",
			expr:   `{!$Record.A}&{!$Record.B}`,
			record: map[string]interface{}{"A": nil, "B": nil},
			want:   nil,
		},
		// Chained concat: "Hello " & null → "Hello "
		{
			name:   "amp_chained_literal_and_null",
			expr:   `"Hello "&{!$Record.Suffix}`,
			record: map[string]interface{}{"Suffix": nil},
			want:   "Hello ",
		},
		// Multi-hop chained: "City" & ", " & null → "City, "
		{
			name:   "amp_three_way_right_null",
			expr:   `{!$Record.City}&", "&{!$Record.Country}`,
			record: map[string]interface{}{"City": "Seoul", "Country": nil},
			want:   "Seoul, ",
		},
		// All non-null: standard concat
		{
			name:   "amp_both_nonnull",
			expr:   `{!$Record.LastName}&{!$Record.FirstName}`,
			record: map[string]interface{}{"LastName": "Solo", "FirstName": " Han"},
			want:   "Solo Han",
		},

		// --- text + with null (string field + null field) ---

		// SF oracle: "Han" + null → "Han" (text + null coerces same as &)
		{
			name:   "text_plus_right_null",
			expr:   `{!$Record.First}+{!$Record.Last}`,
			record: map[string]interface{}{"First": "Han", "Last": nil},
			want:   "Han",
		},

		// --- numeric + with null MUST stay null (regression guard) ---

		// SF oracle: 100.0 + null → null (numeric null propagates)
		{
			name:   "numeric_plus_right_null",
			expr:   `{!$Record.Amount}+{!$Record.Tax}`,
			record: map[string]interface{}{"Amount": 100.0, "Tax": nil},
			want:   nil,
		},
		// SF oracle: null + 5.0 → null
		{
			name:   "numeric_plus_left_null",
			expr:   `{!$Record.Amount}+{!$Record.Tax}`,
			record: map[string]interface{}{"Amount": nil, "Tax": 5.0},
			want:   nil,
		},
		// Numeric both non-null: must still add correctly
		{
			name:   "numeric_plus_both_nonnull",
			expr:   `{!$Record.Amount}+{!$Record.Tax}`,
			record: map[string]interface{}{"Amount": 100.0, "Tax": 5.0},
			want:   105.0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ctx := &Context{Record: tc.record}
			got, err := engine.Evaluate(tc.expr, ctx)
			if err != nil {
				t.Fatalf("Evaluate(%q) error = %v", tc.expr, err)
			}
			if got != tc.want {
				t.Errorf("Evaluate(%q) with record %v = %v (%T), want %v (%T)",
					tc.expr, tc.record, got, got, tc.want, tc.want)
			}
		})
	}
}
