package metadata

import (
	"context"
	"testing"

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

func TestValidationService_ValidateRecord(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, Required: true, Type: constants.FieldTypeString},
			{APIName: "Age", Type: constants.FieldTypeDouble, MinValue: floatPtr(18)},
			{APIName: "Code", Type: constants.FieldTypeString, Regex: strPtr("^[A-Z]{3}$")},
		},
	}

	tests := []struct {
		name      string
		record    models.SObject
		rules     []*models.ValidationRule
		expectErr bool
	}{
		{
			name: "Valid Record",
			record: models.SObject{
				constants.FieldName: "John",
				"Age":               25,
				"Code":              "ABC",
			},
			expectErr: false,
		},
		{
			name: "Missing Required",
			record: models.SObject{
				"Age": 25,
			},
			expectErr: true,
		},
		{
			name: "Too Young",
			record: models.SObject{
				"Name": "Kid", // constants.FieldName usually "Name"
				"Age":  10,
			},
			expectErr: true,
		},
		{
			name: "Invalid Regex",
			record: models.SObject{
				"Name": "BadCode",
				"Code": "abc", // lower case
			},
			expectErr: true,
		},
		{
			name: "Validation Rule Failure",
			record: models.SObject{
				"Name": "RuleBreaker",
				"Age":  20,
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "Age < 21",
					ErrorMessage: "Must be 21 or older",
				},
			},
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := vs.ValidateRecord(tt.record, schema, tt.rules, nil, nil)
			if tt.expectErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func floatPtr(v float64) *float64 { return &v }
func strPtr(v string) *string     { return &v }

// ============================================================================
// Null Keyword Support Tests
// ============================================================================

func TestValidationService_NullKeyword(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "TestObject",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "CloseDate", Type: constants.FieldTypeDate},
		},
	}

	tests := []struct {
		name      string
		record    models.SObject
		rules     []*models.ValidationRule
		expectErr bool
		errMsg    string
	}{
		{
			name: "Null keyword works with nil value",
			record: models.SObject{
				"Name":      "Test",
				"CloseDate": nil,
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "CloseDate == null",
					ErrorMessage: "Close date is required",
				},
			},
			expectErr: true, // Rule triggers because CloseDate IS null
			errMsg:    "Close date is required",
		},
		{
			name: "Null keyword works with present value",
			record: models.SObject{
				"Name":      "Test",
				"CloseDate": "2026-01-15",
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "CloseDate == null",
					ErrorMessage: "Close date is required",
				},
			},
			expectErr: false, // Rule does NOT trigger because CloseDate has a value
		},
		{
			name: "Not null check works",
			record: models.SObject{
				"Name":      "Test",
				"CloseDate": "2026-01-15",
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "CloseDate != null",
					ErrorMessage: "Close date must be empty",
				},
			},
			expectErr: true, // Rule triggers because CloseDate is NOT null
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := vs.ValidateRecord(tt.record, schema, tt.rules, nil, nil)
			if tt.expectErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// ============================================================================
// Empty String Normalization Tests
// ============================================================================

func TestValidationService_EmptyStringNormalization(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "TestObject",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "CloseDate", Type: constants.FieldTypeDate},
			{APIName: "Amount", Type: constants.FieldTypeDouble},
			{APIName: "IsActive", Type: constants.FieldTypeBoolean},
			{APIName: "Description", Type: constants.FieldTypeString}, // Text fields should NOT be normalized
		},
	}

	tests := []struct {
		name      string
		record    models.SObject
		rules     []*models.ValidationRule
		expectErr bool
		errMsg    string
	}{
		{
			name: "Empty date string treated as null",
			record: models.SObject{
				"Name":      "Test",
				"CloseDate": "", // Empty string should become nil
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "CloseDate == null",
					ErrorMessage: "Close date is empty",
				},
			},
			expectErr: true, // Rule triggers because empty string -> nil -> null
		},
		{
			name: "Empty number string treated as null",
			record: models.SObject{
				"Name":   "Test",
				"Amount": "", // Empty string should become nil
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "Amount == null",
					ErrorMessage: "Amount is empty",
				},
			},
			expectErr: true,
		},
		{
			name: "Empty text string NOT treated as null (preserved)",
			record: models.SObject{
				"Name":        "Test",
				"Description": "", // Empty string should be PRESERVED for text
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "Description == null",
					ErrorMessage: "Description is null",
				},
			},
			expectErr: false, // Rule does NOT trigger because text "" is NOT converted to nil
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := vs.ValidateRecord(tt.record, schema, tt.rules, nil, nil)
			if tt.expectErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// ============================================================================
// Fail-Closed Behavior Tests
// ============================================================================

func TestValidationService_FailClosed(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "TestObject",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}

	tests := []struct {
		name      string
		record    models.SObject
		rules     []*models.ValidationRule
		expectErr bool
		errType   string // "validation" or "internal"
	}{
		{
			name:   "Syntax error causes internal error",
			record: models.SObject{"Name": "Test"},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "Name ==== 'Test'", // Invalid syntax
					ErrorMessage: "Should never see this",
				},
			},
			expectErr: true,
			errType:   "internal",
		},
		{
			name:   "Inactive rules are skipped even with bad syntax",
			record: models.SObject{"Name": "Test"},
			rules: []*models.ValidationRule{
				{
					Active:       false,              // Inactive
					Condition:    "Name ==== 'Test'", // Invalid but won't be evaluated
					ErrorMessage: "Should never see this",
				},
			},
			expectErr: false,
		},
		{
			name:   "Valid rule that passes",
			record: models.SObject{"Name": "Test"},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "Name == 'SomeOtherName'", // Condition is false, so no error
					ErrorMessage: "Name mismatch",
				},
			},
			expectErr: false,
		},
		{
			name:   "Valid rule that triggers",
			record: models.SObject{"Name": "Test"},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "Name == 'Test'", // Condition is true, triggers validation error
					ErrorMessage: "Name cannot be Test",
				},
			},
			expectErr: true,
			errType:   "validation",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := vs.ValidateRecord(tt.record, schema, tt.rules, nil, nil)
			if tt.expectErr {
				assert.Error(t, err)
				if tt.errType == "internal" {
					assert.Contains(t, err.Error(), "failed to evaluate")
				} else if tt.errType == "validation" {
					assert.Contains(t, err.Error(), tt.rules[0].ErrorMessage)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// ============================================================================
// Schema Field Population Tests (Issue #219)
// Formulas must be able to reference any schema field, even if not in the record.
// ============================================================================

func TestValidationService_MissingSchemaFieldInFormula(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Lead",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Required: true, Type: constants.FieldTypeString},
			{APIName: "Company", Type: constants.FieldTypeString},
			{APIName: "IsConverted", Type: constants.FieldTypeBoolean},
		},
	}

	tests := []struct {
		name      string
		record    models.SObject
		oldRecord *models.SObject
		rules     []*models.ValidationRule
		expectErr bool
		errMsg    string
	}{
		{
			name: "Rule referencing field not in record evaluates without error (INSERT)",
			record: models.SObject{
				"Name":    "TestLead",
				"Company": "TestCo",
				// IsConverted intentionally omitted — simulates Lead creation
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "IsConverted == true",
					ErrorMessage: "Cannot create converted lead",
				},
			},
			expectErr: false, // IsConverted is nil (not true) → rule does NOT fire
		},
		{
			name: "Rule fires when missing field is nil and condition checks nil",
			record: models.SObject{
				"Name":    "TestLead",
				"Company": "TestCo",
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "IsConverted == nil",
					ErrorMessage: "IsConverted is nil",
				},
			},
			expectErr: true, // IsConverted is nil → condition is true → rule fires
			errMsg:    "IsConverted is nil",
		},
		{
			name: "ISCHANGED on missing field during INSERT returns false",
			record: models.SObject{
				"Name":    "TestLead",
				"Company": "TestCo",
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "ISCHANGED(IsConverted)",
					ErrorMessage: "IsConverted changed",
				},
			},
			expectErr: false, // INSERT context: ISCHANGED always false
		},
		{
			name: "Rule with missing field in UPDATE context (field absent from old record)",
			record: models.SObject{
				"Name":        "TestLead",
				"Company":     "TestCo",
				"IsConverted": true,
			},
			oldRecord: &models.SObject{
				"Name":    "TestLead",
				"Company": "TestCo",
				// IsConverted absent from old record
			},
			rules: []*models.ValidationRule{
				{
					Active:       true,
					Condition:    "ISCHANGED(IsConverted)",
					ErrorMessage: "IsConverted changed",
				},
			},
			expectErr: true, // IsConverted changed from nil to true
			errMsg:    "IsConverted changed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := vs.ValidateRecord(tt.record, schema, tt.rules, tt.oldRecord, nil)
			if tt.expectErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestValidationService_PostConvertReadOnlyRule(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	schema := &models.ObjectMetadata{
		APIName: "Lead",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Required: true, Type: constants.FieldTypeString},
			{APIName: "Company", Type: constants.FieldTypeString},
			{APIName: "IsConverted", Type: constants.FieldTypeBoolean},
			{APIName: "Status", Type: constants.FieldTypeString},
		},
	}
	rules := []*models.ValidationRule{{
		Active:       true,
		Name:         "Lead_Post_Convert_Readonly",
		Condition:    `PRIORVALUE(IsConverted) = TRUE`,
		ErrorMessage: "This lead has already been converted. No further changes are allowed.",
	}}

	convertRecord := models.SObject{
		"Name":        "Test Lead",
		"Company":     "Test Co",
		"IsConverted": true,
		"Status":      "Converted",
	}
	beforeConvert := models.SObject{
		"Name":        "Test Lead",
		"Company":     "Test Co",
		"IsConverted": false,
		"Status":      "Open",
	}
	require.NoError(t, vs.ValidateRecord(convertRecord, schema, rules, &beforeConvert, nil),
		"initial convert transition must be allowed because PRIORVALUE(IsConverted) is false")

	convertedEdit := models.SObject{
		"Name":        "Edited Lead",
		"Company":     "Test Co",
		"IsConverted": true,
		"Status":      "Converted",
	}
	alreadyConverted := models.SObject{
		"Name":        "Test Lead",
		"Company":     "Test Co",
		"IsConverted": true,
		"Status":      "Converted",
	}
	err := vs.ValidateRecord(convertedEdit, schema, rules, &alreadyConverted, nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "This lead has already been converted. No further changes are allowed.")
}

func TestValidationService_BlocksRecordUpdateWithPriorOnlyRules(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	schema := &models.ObjectMetadata{
		APIName: "Lead",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Required: true, Type: constants.FieldTypeString},
			{APIName: "Company", Type: constants.FieldTypeString},
			{APIName: "IsConverted", Type: constants.FieldTypeBoolean},
			{APIName: "Status", Type: constants.FieldTypeString},
		},
	}

	t.Run("prior-only validation rule locks current record", func(t *testing.T) {
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "ReadOnlyAfterState",
			Condition:    `PRIORVALUE(IsConverted) = TRUE`,
			ErrorMessage: "record is locked",
		}}
		record := models.SObject{
			"Name":        "Converted Lead",
			"Company":     "Test Co",
			"IsConverted": true,
			"Status":      "Converted",
		}

		assert.True(t, vs.BlocksRecordUpdateWithPriorOnlyRules(record, schema, rules, nil))
	})

	t.Run("current-value rule remains a save-time validation", func(t *testing.T) {
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "FixableCurrentValue",
			Condition:    `Status = "Bad"`,
			ErrorMessage: "status must be fixed",
		}}
		record := models.SObject{
			"Name":        "Imported Lead",
			"Company":     "Test Co",
			"IsConverted": false,
			"Status":      "Bad",
		}

		assert.False(t, vs.BlocksRecordUpdateWithPriorOnlyRules(record, schema, rules, nil))
	})

	t.Run("mixed prior and current-value rule stays out of access", func(t *testing.T) {
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "MixedRule",
			Condition:    `AND(PRIORVALUE(IsConverted) = TRUE, Status = "Bad")`,
			ErrorMessage: "mixed rule",
		}}
		record := models.SObject{
			"Name":        "Converted Lead",
			"Company":     "Test Co",
			"IsConverted": true,
			"Status":      "Bad",
		}

		assert.False(t, vs.BlocksRecordUpdateWithPriorOnlyRules(record, schema, rules, nil))
	})
}

func TestIsPriorOnlyValidationCondition(t *testing.T) {
	tests := []struct {
		name      string
		condition string
		want      bool
	}{
		{
			name:      "empty",
			condition: "",
			want:      false,
		},
		{
			name:      "whitespace only",
			condition: "   \n\t",
			want:      false,
		},
		{
			name:      "pure prior value comparison",
			condition: `PRIORVALUE(IsConverted) = TRUE`,
			want:      true,
		},
		{
			name:      "prior value wrapped in allowed functions",
			condition: `AND(PRIORVALUE(IsConverted) = TRUE, NOT(ISBLANK(PRIORVALUE(Status))))`,
			want:      true,
		},
		{
			name:      "string literal identifier is ignored",
			condition: `OR(PRIORVALUE(Status) = "Bad Status", PRIORVALUE(IsConverted) = TRUE)`,
			want:      true,
		},
		{
			name:      "mixed prior and current field",
			condition: `AND(PRIORVALUE(IsConverted) = TRUE, Status = "Bad")`,
			want:      false,
		},
		{
			name:      "unrecognized function alongside prior value",
			condition: `ISPICKVAL(PRIORVALUE(Status), "Converted")`,
			want:      false,
		},
		{
			name:      "field identifier outside prior value",
			condition: `PRIORVALUE(IsConverted) = TRUE && OwnerId != NULL`,
			want:      false,
		},
		{
			name:      "unsupported dotted prior expression remains current shape",
			condition: `PRIORVALUE(Account.OwnerId) != NULL`,
			want:      false,
		},
	}

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

func TestValidationService_EmptyStringPatternFields(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		Fields: []models.FieldMetadata{
			{APIName: "Name", Required: true, Type: constants.FieldTypeString},
			{APIName: "Website__c", Type: constants.FieldTypeUrl},
			{APIName: "Email__c", Type: constants.FieldTypeEmail},
			{APIName: "Phone__c", Type: constants.FieldTypePhone},
		},
	}

	tests := []struct {
		name      string
		record    models.SObject
		expectErr bool
	}{
		{
			name:      "Empty URL field should pass (non-required)",
			record:    models.SObject{"Name": "Test", "Website__c": ""},
			expectErr: false,
		},
		{
			name:      "Empty email field should pass (non-required)",
			record:    models.SObject{"Name": "Test", "Email__c": ""},
			expectErr: false,
		},
		{
			name:      "Empty phone field should pass (non-required)",
			record:    models.SObject{"Name": "Test", "Phone__c": ""},
			expectErr: false,
		},
		{
			name:      "Valid URL with https should pass",
			record:    models.SObject{"Name": "Test", "Website__c": "https://example.com"},
			expectErr: false,
		},
		{
			name:      "Valid URL without protocol should pass (issue #1829)",
			record:    models.SObject{"Name": "Test", "Website__c": "www.example.com"},
			expectErr: false,
		},
		{
			name:      "Invalid URL with whitespace should fail",
			record:    models.SObject{"Name": "Test", "Website__c": "not a url"},
			expectErr: true,
		},
		{
			name:      "Nil URL field should pass",
			record:    models.SObject{"Name": "Test", "Website__c": nil},
			expectErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := vs.ValidateRecord(tt.record, schema, nil, nil, nil)
			if tt.expectErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// ============================================================================
// Salesforce Import Artifact Detection Tests (Fixes #100)
// ============================================================================

func TestIsSalesforceImportArtifact(t *testing.T) {
	tests := []struct {
		ident    string
		expected bool
		reason   string
	}{
		{"Partner_Margin__c", true, "standard custom field"},
		{"IsConvertQuote__c", true, "standard custom field"},
		{"My_Field__c", true, "standard custom field"},
		{"awsapn__APN_CRM_Id__c", true, "namespace-prefixed custom field"},
		{"Elvance__Elvance_Owner__c", true, "namespace-prefixed custom field"},
		{"INCLUDES", false, "INCLUDES is now implemented"},
		{"REGEX", true, "known unsupported function"},
		{"RecordType", false, "now supported relationship"},
		{"Owner", false, "now supported relationship"},
		{"SomePlainField", false, "regular field identifier"},
		{"Name", false, "standard field"},
		{"Amount", false, "standard field"},
	}

	for _, tt := range tests {
		t.Run(tt.ident, func(t *testing.T) {
			result := isSalesforceImportArtifact(tt.ident)
			assert.Equal(t, tt.expected, result, "ident=%q (%s)", tt.ident, tt.reason)
		})
	}
}

func TestValidationService_SkipsRuleWithMissingCustomField(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Quote",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "Status", Type: constants.FieldTypeString},
		},
	}

	// Rule references Partner_Margin__c which does not exist in schema/record
	rules := []*models.ValidationRule{
		{
			Active:       true,
			Name:         "Partner_Margin_should_have_value",
			Condition:    "AND(ISBLANK(Partner_Margin__c), LEN(Partner_Name__c)>5, NOT(TRUTHY(IsConvertQuote__c)))",
			ErrorMessage: "Partner Margin is required",
		},
	}

	record := models.SObject{
		"Name":   "Test Quote",
		"Status": "Draft",
	}

	err := vs.ValidateRecord(record, schema, rules, nil, nil)
	assert.NoError(t, err, "rule referencing missing __c field should be skipped, not fail-closed")
}

func TestValidationService_DoesNotSkipRuleForKnownCustomFieldMissingFromPayload(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Quote",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "Partner_Margin__c", Type: constants.FieldTypeString},
		},
	}

	rules := []*models.ValidationRule{
		{
			Active:       true,
			Name:         "Partner_Margin_required",
			Condition:    "ISBLANK(Partner_Margin__c)",
			ErrorMessage: "Partner Margin is required",
		},
	}

	record := models.SObject{
		"Name": "Test Quote",
	}

	err := vs.ValidateRecord(record, schema, rules, nil, nil)
	assert.Error(t, err, "known schema fields should still participate in validation when omitted from the payload")
	assert.Contains(t, err.Error(), "Partner Margin is required")
}

func TestValidationService_RequiredFieldsOnUpdate(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Required: true, Type: constants.FieldTypeString},
			{APIName: "Phone", Type: constants.FieldTypePhone},
		},
	}

	t.Run("rejects explicit clear of required field", func(t *testing.T) {
		oldRecord := models.SObject{
			"Name":  "Acme",
			"Phone": "+15551230001",
		}
		recordToValidate := models.SObject{
			"Name":  "",
			"Phone": "+15551230001",
		}
		err := vs.ValidateRecord(recordToValidate, schema, nil, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Name")
		assert.Contains(t, err.Error(), "is required")
	})

	t.Run("allows unrelated update when required field is unchanged", func(t *testing.T) {
		oldRecord := models.SObject{
			"Name":  "Acme",
			"Phone": "+15551230001",
		}
		recordToValidate := models.SObject{
			"Name":  "Acme",
			"Phone": "+15551230002",
		}
		err := vs.ValidateRecord(recordToValidate, schema, nil, &oldRecord, nil)
		assert.NoError(t, err)
	})

	t.Run("rejects unrelated update when legacy record is already missing required field", func(t *testing.T) {
		oldRecord := models.SObject{
			"Phone": "+15551230001",
		}
		recordToValidate := models.SObject{
			"Phone": "+15551230002",
		}

		err := vs.ValidateRecord(recordToValidate, schema, nil, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Name")
		assert.Contains(t, err.Error(), "is required")
	})
}

func TestValidationService_NullFormulaSemanticsForCoercedFields(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "Probability", Type: constants.FieldTypePercent},
			{APIName: "Flag", Type: constants.FieldTypeBoolean},
		},
	}

	t.Run("explicit null numeric still matches equality to null", func(t *testing.T) {
		record := models.SObject{
			"Name":        "Deal",
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Probability_null_rule",
			Condition:    "Probability == null",
			ErrorMessage: "Probability must not be null",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Probability must not be null")
	})

	t.Run("explicit null numeric still matches ISBLANK", func(t *testing.T) {
		record := models.SObject{
			"Name":        "Deal",
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Probability_blank_rule",
			Condition:    "ISBLANK(Probability)",
			ErrorMessage: "Probability is blank",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Probability is blank")
	})

	t.Run("explicit null boolean still matches equality to null", func(t *testing.T) {
		record := models.SObject{
			"Name": "Deal",
			"Flag": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Flag_null_rule",
			Condition:    "Flag == null",
			ErrorMessage: "Flag must not be null",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Flag must not be null")
	})

	t.Run("missing numeric field still compares safely as zero", func(t *testing.T) {
		record := models.SObject{
			"Name": "Deal",
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Probability_threshold_rule",
			Condition:    "Probability >= 0.4",
			ErrorMessage: "Probability threshold reached",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.NoError(t, err)
	})

	t.Run("ISCHANGED detects null to zero transition", func(t *testing.T) {
		record := models.SObject{
			"Name":        "Deal",
			"Probability": float64(0),
		}
		oldRecord := models.SObject{
			"Name":        "Deal",
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Probability_changed_rule",
			Condition:    "ISCHANGED(Probability)",
			ErrorMessage: "Probability changed",
		}}

		err := vs.ValidateRecord(record, schema, rules, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Probability changed")
	})

	t.Run("ISCHANGED detects null to false transition", func(t *testing.T) {
		record := models.SObject{
			"Name": "Deal",
			"Flag": false,
		}
		oldRecord := models.SObject{
			"Name": "Deal",
			"Flag": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Flag_changed_rule",
			Condition:    "ISCHANGED(Flag)",
			ErrorMessage: "Flag changed",
		}}

		err := vs.ValidateRecord(record, schema, rules, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Flag changed")
	})

	t.Run("PRIORVALUE numeric null still matches equality to null", func(t *testing.T) {
		record := models.SObject{
			"Name":        "Deal",
			"Probability": float64(0),
		}
		oldRecord := models.SObject{
			"Name":        "Deal",
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Probability_prior_null_rule",
			Condition:    "PRIORVALUE(Probability) == null",
			ErrorMessage: "Prior probability was null",
		}}

		err := vs.ValidateRecord(record, schema, rules, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Prior probability was null")
	})

	t.Run("ISBLANK on PRIORVALUE boolean still treats prior null as blank", func(t *testing.T) {
		record := models.SObject{
			"Name": "Deal",
			"Flag": false,
		}
		oldRecord := models.SObject{
			"Name": "Deal",
			"Flag": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "Flag_prior_blank_rule",
			Condition:    "ISBLANK(PRIORVALUE(Flag))",
			ErrorMessage: "Prior flag was blank",
		}}

		err := vs.ValidateRecord(record, schema, rules, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "Prior flag was blank")
	})
}

func TestValidationService_FormulaEvaluationDoesNotMutateInputRecord(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "Probability", Type: constants.FieldTypePercent},
			{APIName: "Flag", Type: constants.FieldTypeBoolean},
			{APIName: "Score", Type: constants.FieldTypeDouble},
		},
	}

	record := models.SObject{
		"Name":        "Deal",
		"Probability": nil,
		"Flag":        nil,
	}
	rules := []*models.ValidationRule{{
		Active:       true,
		Name:         "Safe_formula_eval",
		Condition:    "Probability >= 0.4 || Flag == true || Score >= 1",
		ErrorMessage: "should not fire",
	}}

	err := vs.ValidateRecord(record, schema, rules, nil, nil)
	assert.NoError(t, err)
	assert.Contains(t, record, "Probability")
	assert.Nil(t, record["Probability"])
	assert.Contains(t, record, "Flag")
	assert.Nil(t, record["Flag"])
	_, exists := record["Score"]
	assert.False(t, exists)
}

func TestValidationService_ValidationFormulaRewriteEdgeCases(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{APIName: "Probability", Type: constants.FieldTypePercent},
		},
	}

	t.Run("quoted string literals are not rewritten", func(t *testing.T) {
		assert.Equal(t,
			`Name == "Probability == null"`,
			rewriteValidationFormulaExpression(`Name == "Probability == null"`, schema),
		)

		record := models.SObject{
			"Name": "Probability == null",
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "quoted_literal_rule",
			Condition:    `Name == "Probability == null"`,
			ErrorMessage: "quoted literal matched",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "quoted literal matched")
	})

	t.Run("null on the left matches explicit null current values", func(t *testing.T) {
		assert.Equal(t,
			"ISNULLX(Probability, __null_Probability)",
			rewriteValidationFormulaExpression("null == Probability", schema),
		)

		record := models.SObject{
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "null_left_eq_rule",
			Condition:    "null == Probability",
			ErrorMessage: "left null equality matched",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "left null equality matched")
	})

	t.Run("null on the left does not misfire inequality for explicit null current values", func(t *testing.T) {
		assert.Equal(t,
			"NOT(ISNULLX(Probability, __null_Probability))",
			rewriteValidationFormulaExpression("null != Probability", schema),
		)

		record := models.SObject{
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "null_left_ne_rule",
			Condition:    "null != Probability",
			ErrorMessage: "left null inequality matched",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		assert.NoError(t, err)
	})

	t.Run("null on the left matches explicit null prior values", func(t *testing.T) {
		assert.Equal(t,
			"ISNULLX(_prior_Probability, __prior_null_Probability)",
			rewriteValidationFormulaExpression("null == PRIORVALUE(Probability)", schema),
		)

		record := models.SObject{
			"Probability": float64(0),
		}
		oldRecord := models.SObject{
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "prior_null_left_eq_rule",
			Condition:    "null == PRIORVALUE(Probability)",
			ErrorMessage: "left prior null equality matched",
		}}

		err := vs.ValidateRecord(record, schema, rules, &oldRecord, nil)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "left prior null equality matched")
	})

	t.Run("null on the left does not misfire inequality for explicit null prior values", func(t *testing.T) {
		assert.Equal(t,
			"NOT(ISNULLX(_prior_Probability, __prior_null_Probability))",
			rewriteValidationFormulaExpression("null != PRIORVALUE(Probability)", schema),
		)

		record := models.SObject{
			"Probability": float64(0),
		}
		oldRecord := models.SObject{
			"Probability": nil,
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "prior_null_left_ne_rule",
			Condition:    "null != PRIORVALUE(Probability)",
			ErrorMessage: "left prior null inequality matched",
		}}

		err := vs.ValidateRecord(record, schema, rules, &oldRecord, nil)
		assert.NoError(t, err)
	})
}

// TestValidationService_MultiPicklistOptionValueNoSemicolon pins the follow-up
// constraint for issue #1686 / PR #1684. Salesforce API v63.0 uses `;` as the
// multi-picklist wire separator, and `FromStorageRecord` joins stored JSON
// arrays with `;` to match. An option whose Value contains `;` cannot be
// round-tripped and, if a record-triggered flow writes the field, re-opens the
// self-update loop fixed by #1684. ValidateFieldMetadata must reject such
// option values at the authoring boundary.
func TestValidationService_MultiPicklistOptionValueNoSemicolon(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	t.Run("rejects multi-picklist option value containing semicolon", func(t *testing.T) {
		err := vs.ValidateFieldMetadata(&models.FieldMetadata{
			APIName: "Tags__c", Type: constants.FieldTypeMultiPicklist,
			Options: models.PicklistOptions{
				{Value: "Alpha", Label: "Alpha"},
				{Value: "Needs Review; Urgent", Label: "Needs Review; Urgent"},
			},
		})
		require.Error(t, err)
		assert.Contains(t, err.Error(), ";")
	})

	t.Run("accepts multi-picklist options without semicolon", func(t *testing.T) {
		err := vs.ValidateFieldMetadata(&models.FieldMetadata{
			APIName: "Tags__c", Type: constants.FieldTypeMultiPicklist,
			Options: models.PicklistOptions{
				{Value: "Alpha", Label: "Alpha"},
				{Value: "Beta Gamma", Label: "Beta Gamma"},
				{Value: "With Comma, OK", Label: "With Comma"},
			},
		})
		require.NoError(t, err)
	})

	t.Run("rejects semicolon even if only trailing or embedded in one option", func(t *testing.T) {
		cases := []string{"A;", ";B", "A;B", "A ; B"}
		for _, v := range cases {
			err := vs.ValidateFieldMetadata(&models.FieldMetadata{
				APIName: "Tags__c", Type: constants.FieldTypeMultiPicklist,
				Options: models.PicklistOptions{{Value: v, Label: v}},
			})
			assert.Error(t, err, "expected error for option value %q", v)
		}
	})

	t.Run("single picklist is not constrained (single-value fields do not use ; separator)", func(t *testing.T) {
		err := vs.ValidateFieldMetadata(&models.FieldMetadata{
			APIName: "Stage", Type: constants.FieldTypePicklist,
			Options: models.PicklistOptions{
				{Value: "In; Progress", Label: "In; Progress"},
			},
		})
		require.NoError(t, err)
	})

	t.Run("non-picklist field with semicolon-bearing default is not affected", func(t *testing.T) {
		dv := "A; B"
		err := vs.ValidateFieldMetadata(&models.FieldMetadata{
			APIName: "Note", Type: constants.FieldTypeString, DefaultValue: &dv,
		})
		require.NoError(t, err)
	})
}

func TestValidationService_CrossObjectValidationRules(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	vs.SetFormulaRelationResolver(func(_ context.Context, objectAPIName string, record models.SObject, relationName string) (models.SObject, error) {
		if objectAPIName != "Opportunity" {
			return nil, nil
		}
		switch relationName {
		case "RecordType":
			if record["RecordTypeId"] == "rt-partner" {
				return models.SObject{
					constants.FieldID:   "rt-partner",
					constants.FieldName: "Partner Opportunity",
					"DeveloperName":     "Partner",
				}, nil
			}
		case "Owner":
			if record["OwnerId"] == "user-1" {
				return models.SObject{
					constants.FieldID:   "user-1",
					constants.FieldName: "Alice Admin",
				}, nil
			}
		}
		return nil, nil
	})

	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, Type: constants.FieldTypeString},
			{APIName: "RecordTypeId", Type: constants.FieldTypeReference, ReferenceTo: []string{"RecordType"}},
			{APIName: "OwnerId", Type: constants.FieldTypeReference, ReferenceTo: []string{constants.TableUser}},
		},
	}

	t.Run("RecordType.DeveloperName no longer skips evaluation", func(t *testing.T) {
		record := models.SObject{
			constants.FieldName: "Deal",
			"RecordTypeId":      "rt-partner",
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "RequirePartnerRule",
			Condition:    `RecordType.DeveloperName == "Partner"`,
			ErrorMessage: "partner opportunities are blocked",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "partner opportunities are blocked")
	})

	t.Run("Owner.Name no longer skips evaluation", func(t *testing.T) {
		record := models.SObject{
			constants.FieldName: "Deal",
			"OwnerId":           "user-1",
		}
		rules := []*models.ValidationRule{{
			Active:       true,
			Name:         "OwnerRule",
			Condition:    `Owner.Name == "Alice Admin"`,
			ErrorMessage: "owner-based rule fired",
		}}

		err := vs.ValidateRecord(record, schema, rules, nil, nil)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "owner-based rule fired")
	})
}

func TestValidationService_Naming(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())

	tests := []struct {
		name      string
		apiName   string
		expectErr bool
	}{
		{"Valid", "valid_name_123", false},
		{"CamelCase", "BadName", false},
		{"Spaces", "bad name", true},
		{"Dashes", "bad-name", true},
		{"StartNum", "1bad", true},
	}

	for _, tt := range tests {
		t.Run("Object_"+tt.name, func(t *testing.T) {
			err := vs.ValidateObjectMetadata(&models.ObjectMetadata{APIName: tt.apiName})
			if tt.expectErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
		t.Run("Field_"+tt.name, func(t *testing.T) {
			err := vs.ValidateFieldMetadata(&models.FieldMetadata{APIName: tt.apiName})
			if tt.expectErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
