package formula

import (
	"strings"
	"testing"

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

// TestToSQL_SalesforceOperators covers the filter expressions that Salesforce
// Flow metadata (and our own executor_helpers.buildFilterFromConditionsImpl)
// produce for recordLookup filters. Before the fix the expression parser
// rejected uppercase AND/OR/NOT used as infix operators and `IS NULL` /
// `IS NOT NULL` tokens. See pingcap-inc/MetaDrivenPlatform4AI#1124.
func TestToSQL_SalesforceOperators(t *testing.T) {
	tests := []struct {
		name       string
		expression string
		// Whitespace-insensitive substrings we expect to appear in the generated
		// SQL. Kept as substrings because the precise parenthesization is an
		// implementation detail of the walker.
		wantSQLContains []string
		wantArgs        []interface{}
	}{
		{
			name:            "issue_1124_and_paren_no_space",
			expression:      "Name == 'PatchTest Co' AND(Name IS NULL OR Name == '')",
			wantSQLContains: []string{"Name = ?", "Name IS NULL", "AND", "OR"},
			wantArgs:        []interface{}{"PatchTest Co", ""},
		},
		{
			name:            "issue_1124_and_paren_with_space",
			expression:      "Name == 'PatchTest Co' AND (Name IS NULL OR Name == '')",
			wantSQLContains: []string{"Name = ?", "Name IS NULL", "AND", "OR"},
			wantArgs:        []interface{}{"PatchTest Co", ""},
		},
		{
			name:            "uppercase_AND_infix",
			expression:      "Name == 'X' AND Status == 'Open'",
			wantSQLContains: []string{"Name = ?", "Status = ?", "AND"},
			wantArgs:        []interface{}{"X", "Open"},
		},
		{
			name:            "uppercase_OR_infix",
			expression:      "Status == 'New' OR Status == 'Prospecting'",
			wantSQLContains: []string{"Status = ?", "OR"},
			wantArgs:        []interface{}{"New", "Prospecting"},
		},
		{
			name:            "uppercase_OR_paren_no_space",
			expression:      "Status == 'A' OR(Status == 'B')",
			wantSQLContains: []string{"Status = ?", "OR"},
			wantArgs:        []interface{}{"A", "B"},
		},
		{
			name:            "uppercase_NOT_paren_no_space",
			expression:      "NOT(Status == 'Closed')",
			wantSQLContains: []string{"NOT", "Status = ?"},
			wantArgs:        []interface{}{"Closed"},
		},
		{
			name:            "mixed_case_and",
			expression:      "Name == 'X' And Status == 'Open'",
			wantSQLContains: []string{"Name = ?", "Status = ?", "AND"},
			wantArgs:        []interface{}{"X", "Open"},
		},
		{
			name:            "is_null_suffix",
			expression:      "Email IS NULL",
			wantSQLContains: []string{"Email IS NULL"},
			wantArgs:        []interface{}{},
		},
		{
			name:            "is_not_null_suffix",
			expression:      "Email IS NOT NULL",
			wantSQLContains: []string{"Email IS NOT NULL"},
			wantArgs:        []interface{}{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sql, args, err := ToSQL(tt.expression)
			require.NoError(t, err, "expression %q should parse", tt.expression)
			for _, want := range tt.wantSQLContains {
				assert.Contains(t, sql, want, "SQL %q missing substring %q for input %q", sql, want, tt.expression)
			}
			assert.Equal(t, tt.wantArgs, args, "args mismatch for %q (sql=%s)", tt.expression, sql)
		})
	}
}

// TestToSQL_Issue1124_ExactErrorExpression reproduces the exact filterExpr
// from the bug report. Before the fix this returned
// `unexpected token Identifier("AND") (1:24)`.
func TestToSQL_Issue1124_ExactErrorExpression(t *testing.T) {
	// This is the shape that executor_helpers.buildFilterFromConditionsImpl
	// produces for [EqualTo 'PatchTest Co' AND IsNull AND NotEqualTo '...'] —
	// namely `Name == 'X' AND (Name IS NULL OR Name == '') AND Name != '...'`.
	expr := "Name == 'PatchTest Co' AND (Name IS NULL OR Name == '') AND Name != 'Z'"
	sql, args, err := ToSQL(expr)
	require.NoError(t, err)
	assert.Contains(t, sql, "Name = ?")
	assert.Contains(t, sql, "Name IS NULL")
	assert.Contains(t, sql, "Name != ?")
	// "PatchTest Co", "", "Z" — args order follows walk order
	assert.Equal(t, []interface{}{"PatchTest Co", "", "Z"}, args)
	// Sanity: `AND` / `OR` appear as SQL logical operators (not as uppercase
	// identifiers).
	assert.Greater(t, strings.Count(sql, "AND"), 0)
	assert.Greater(t, strings.Count(sql, "OR"), 0)
}

// TestToSQL_Issue1128_SFEqualityWithAndOrIsNull exercises the regression
// fixed in #1128: when the input still uses Salesforce single-`=` equality
// alongside `AND` / `OR` / `IS NULL`, the AND/OR/IS-NULL rewrites must run
// AFTER replaceFormulaOperators has converted `=` to `==`. Otherwise the
// pipeline produces malformed expressions that downstream Evaluate cannot
// parse, surfacing as HTTP 400 on PATCH /sobjects/QuoteLineItem/...
// (validation rules being the typical caller).
func TestToSQL_Issue1128_SFEqualityWithAndOrIsNull(t *testing.T) {
	// Salesforce single-`=` equality combined with infix AND, infix OR, and
	// IS NULL — the exact mix that broke in #1128.
	expr := "Status = 'Open' AND Quantity = 1 OR Description IS NULL"
	sql, args, err := ToSQL(expr)
	require.NoError(t, err, "expression %q should parse end-to-end", expr)
	assert.Contains(t, sql, "Status = ?")
	assert.Contains(t, sql, "Quantity = ?")
	assert.Contains(t, sql, "Description IS NULL")
	assert.Contains(t, sql, "AND")
	assert.Contains(t, sql, "OR")
	assert.Equal(t, []interface{}{"Open", 1}, args)
}
