package metadata

import (
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"
	"time"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

type validationRecordSetQueryServiceStub struct {
	ports.QueryService
	calls   []string
	specs   []queryspec.QuerySpec
	handler func(filterExpr string) (*models.QueryResult, error)
}

func (s *validationRecordSetQueryServiceStub) QueryWithSpec(_ context.Context, req queryspec.QuerySpec, _ *models.UserSession) (*models.QueryResult, error) {
	s.calls = append(s.calls, req.ObjectAPIName+"|"+req.Filter.Expression)
	s.specs = append(s.specs, req)
	if req.Limit != validationRecordSetBindingQueryLimit {
		return nil, errors.New("unexpected record-set query limit")
	}
	return s.handler(req.Filter.Expression)
}

func validationRecordSetMetadataService(schemas ...*models.ObjectMetadata) *MetadataService {
	schemaMap := make(map[string]*models.ObjectMetadata, len(schemas))
	for _, schema := range schemas {
		schemaMap[strings.ToLower(schema.APIName)] = schema
	}
	service := &MetadataService{}
	service.coreCache.Store(&metadataSnapshot{schemas: schemas, schemaMap: schemaMap})
	return service
}

func TestValidateRecordReusesIdenticalRecordSetCandidateQueryWithinOneInvocation(t *testing.T) {
	rootSchema := issueSchema()
	query := &validationRecordSetQueryServiceStub{
		handler: func(string) (*models.QueryResult, error) {
			return &models.QueryResult{
				TotalSize: 1,
				Done:      true,
				Records:   []models.SObject{{"Id": "P1"}},
			}, nil
		},
	}
	vs := NewValidationService(formula.NewEngine())
	vs.SetRecordSetBindingResolver(NewValidationRecordSetBindingResolver(
		validationRecordSetMetadataService(rootSchema, projectSchema()),
		query,
		nil,
	))

	first := projectRule()
	first.Name = "First_Project_Check"
	first.RecordSetBindings[0].Name = "FirstHasProject"
	first.Condition = "NOT(FirstHasProject)"
	second := projectRule()
	second.Name = "Second_Project_Check"
	second.RecordSetBindings[0].Name = "SecondHasProject"
	second.Condition = "NOT(SecondHasProject)"
	record := models.SObject{"Subject": "Deploy", "ProjectId": "P1"}

	if err := vs.ValidateRecord(context.Background(), record, rootSchema, []*models.ValidationRule{first, second}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord() error = %v, want nil", err)
	}
	if len(query.calls) != 1 {
		t.Fatalf("QueryWithFilter() calls = %d, want 1 shared candidate read", len(query.calls))
	}

	if err := vs.ValidateRecord(context.Background(), record, rootSchema, []*models.ValidationRule{first, second}, nil, nil); err != nil {
		t.Fatalf("second ValidateRecord() error = %v, want nil", err)
	}
	if len(query.calls) != 2 {
		t.Fatalf("QueryWithFilter() calls across two validation invocations = %d, want 2", len(query.calls))
	}
	if got := query.specs[0].ReferenceHydration; got != queryspec.ReferenceHydrationExplicitOnly {
		t.Fatalf("ReferenceHydration = %v, want explicit-only", got)
	}
	if got, want := query.specs[0].SelectFields, []string{constants.FieldID}; !reflect.DeepEqual(got, want) {
		t.Fatalf("SelectFields = %v, want %v", got, want)
	}
}

func TestValidationRecordSetBindingSelectFieldsKeepsOnlyFilterClosure(t *testing.T) {
	binding := models.ValidationRuleRecordSetBinding{Filters: []models.ValidationRuleRecordSetFilter{
		{Field: "AccountId"},
		{Field: "RecordType.Name"},
		{Field: "accountid"},
		{Field: "Status"},
	}}
	got := validationRecordSetBindingSelectFields(binding)
	want := []string{constants.FieldID, "AccountId", "RecordType.Name", "Status"}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("validationRecordSetBindingSelectFields = %v, want %v", got, want)
	}
}

func TestValidateRecordDoesNotCollideDistinctRecordSetCandidateQueries(t *testing.T) {
	rootSchema := issueSchema()
	rootSchema.Fields = append(rootSchema.Fields, models.FieldMetadata{
		APIName: "OtherProjectId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Project"},
	})
	query := &validationRecordSetQueryServiceStub{
		handler: func(filterExpr string) (*models.QueryResult, error) {
			id := "P1"
			if strings.Contains(filterExpr, "P2") {
				id = "P2"
			}
			return &models.QueryResult{TotalSize: 1, Done: true, Records: []models.SObject{{"Id": id}}}, nil
		},
	}
	vs := NewValidationService(formula.NewEngine())
	vs.SetRecordSetBindingResolver(NewValidationRecordSetBindingResolver(
		validationRecordSetMetadataService(rootSchema, projectSchema()),
		query,
		nil,
	))

	first := projectRule()
	first.Name = "First_Project_Check"
	first.RecordSetBindings[0].Name = "FirstHasProject"
	first.Condition = "NOT(FirstHasProject)"
	second := projectRule()
	second.Name = "Second_Project_Check"
	second.RecordSetBindings[0].Name = "SecondHasProject"
	second.RecordSetBindings[0].Filters[0].Value = "OtherProjectId"
	second.Condition = "NOT(SecondHasProject)"

	err := vs.ValidateRecord(
		context.Background(),
		models.SObject{"Subject": "Deploy", "ProjectId": "P1", "OtherProjectId": "P2"},
		rootSchema,
		[]*models.ValidationRule{first, second},
		nil,
		nil,
	)
	if err != nil {
		t.Fatalf("ValidateRecord() error = %v, want nil", err)
	}
	if len(query.calls) != 2 {
		t.Fatalf("QueryWithFilter() calls = %d, want 2 distinct candidate reads", len(query.calls))
	}
}

func TestValidateRecordRechecksRuleSpecificRelationshipFiltersAfterCandidateCacheHit(t *testing.T) {
	rootSchema := issueSchema()
	recordTypeRelationship := constants.ObjectRecordType
	candidateSchema := projectSchema()
	candidateSchema.Fields = append(candidateSchema.Fields, models.FieldMetadata{
		APIName:          constants.FieldRecordTypeID,
		Type:             constants.FieldTypeReference,
		ReferenceTo:      []string{constants.ObjectRecordType},
		RelationshipName: &recordTypeRelationship,
	})
	query := &validationRecordSetQueryServiceStub{
		handler: func(string) (*models.QueryResult, error) {
			return &models.QueryResult{
				TotalSize: 1,
				Done:      true,
				Records: []models.SObject{{
					"Id":                        "P1",
					constants.FieldRecordTypeID: "RT1",
				}},
			}, nil
		},
	}
	vs := NewValidationService(formula.NewEngine())
	vs.SetFormulaRelationResolver(func(_ context.Context, _ string, _ models.SObject, relationName string) (models.SObject, error) {
		if relationName != constants.ObjectRecordType {
			return nil, errors.New("unexpected relation")
		}
		return models.SObject{
			constants.FieldID:   "RT1",
			constants.FieldName: "Contract_Type_A",
			"DeveloperName":     "Contract_Type_A",
		}, nil
	})
	vs.SetRecordSetBindingResolver(NewValidationRecordSetBindingResolver(
		validationRecordSetMetadataService(rootSchema, candidateSchema),
		query,
		vs.relationResolve,
	))

	newRule := func(name, bindingName, expectedType, condition string) *models.ValidationRule {
		binding := bindingWithFilter(bindingName)
		binding.Filters = append(binding.Filters, models.ValidationRuleRecordSetFilter{
			Field:       "RecordType.DeveloperName",
			Operator:    "equals",
			ValueSource: "literal",
			Value:       expectedType,
		})
		return &models.ValidationRule{
			Name:              name,
			Active:            true,
			Condition:         condition,
			ErrorMessage:      "unexpected relationship result",
			RecordSetBindings: []models.ValidationRuleRecordSetBinding{binding},
		}
	}
	rules := []*models.ValidationRule{
		newRule("Type_A_Must_Exist", "HasTypeA", "Contract_Type_A", "NOT(HasTypeA)"),
		newRule("Type_B_Must_Not_Exist", "HasTypeB", "Contract_Type_B", "HasTypeB"),
	}

	if err := vs.ValidateRecord(
		context.Background(),
		models.SObject{"Subject": "Deploy", "ProjectId": "P1"},
		rootSchema,
		rules,
		nil,
		nil,
	); err != nil {
		t.Fatalf("ValidateRecord() error = %v, want nil", err)
	}
	if len(query.calls) != 1 {
		t.Fatalf("QueryWithFilter() calls = %d, want one shared pushdown candidate read", len(query.calls))
	}
}

func TestValidationRecordSetBindingQueryCachesOnlySuccessfulDetachedResults(t *testing.T) {
	ctx := withValidationRecordSetBindingQueryCache(context.Background())
	key := validationRecordSetBindingQueryKey{objectAPIName: "Project", filterExpr: `Id == "P1"`, limit: 500}
	calls := 0
	wantErr := errors.New("temporary query failure")
	load := func() (*models.QueryResult, error) {
		calls++
		if calls == 1 {
			return nil, wantErr
		}
		return &models.QueryResult{TotalSize: 1, Done: true, Records: []models.SObject{{"Id": "P1"}}}, nil
	}

	if _, err := validationRecordSetBindingQuery(ctx, key, load); !errors.Is(err, wantErr) {
		t.Fatalf("first query error = %v, want %v", err, wantErr)
	}
	first, err := validationRecordSetBindingQuery(ctx, key, load)
	if err != nil {
		t.Fatalf("second query error = %v, want nil", err)
	}
	first.Records[0]["Id"] = "mutated"
	second, err := validationRecordSetBindingQuery(ctx, key, load)
	if err != nil {
		t.Fatalf("cached query error = %v, want nil", err)
	}
	if calls != 2 {
		t.Fatalf("load calls = %d, want 2 (failure not cached, success cached)", calls)
	}
	if got := second.Records[0]["Id"]; got != "P1" {
		t.Fatalf("cached detached record Id = %#v, want P1", got)
	}
}

func TestValidationRuleRecordSetBindingBlocksWhenExistsFalse(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	vs.SetRecordSetBindingResolver(func(_ context.Context, _ *models.ObjectMetadata, _ models.SObject, _ *models.ValidationRule, binding models.ValidationRuleRecordSetBinding, _ map[string]interface{}) (bool, error) {
		if binding.Name != "HasProject" {
			t.Fatalf("binding name = %q, want HasProject", binding.Name)
		}
		return false, nil
	})

	err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy"}, issueSchema(), []*models.ValidationRule{projectRule()}, nil, nil)
	var ruleErr *apperrors.ValidationRuleError
	if !errors.As(err, &ruleErr) {
		t.Fatalf("ValidateRecord error = %T %v, want ValidationRuleError", err, err)
	}
	if ruleErr.Message != "Project is required" {
		t.Fatalf("ValidationRuleError message = %q", ruleErr.Message)
	}
}

func TestValidationRuleRecordSetBindingAllowsWhenExistsTrue(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		return true, nil
	})

	if err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy"}, issueSchema(), []*models.ValidationRule{projectRule()}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord error = %v, want nil", err)
	}
}

func TestValidationRuleRecordSetBindingSkipsQueryWhenConditionIsBindingIndependentFalse(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	calls := 0
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		calls++
		return false, nil
	})
	rule := projectRule()
	rule.Condition = `Status == "Activated" && NOT(HasProject)`
	schema := issueSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{APIName: "Status", Type: constants.FieldTypeString})

	if err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy", "Status": "Draft"}, schema, []*models.ValidationRule{rule}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord error = %v, want nil", err)
	}
	if calls != 0 {
		t.Fatalf("record-set resolver calls = %d, want 0 for a false binding-independent guard", calls)
	}
}

func TestValidationRuleRecordSetBindingStillQueriesWhenBindingCanChangeOutcome(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	calls := 0
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		calls++
		return true, nil
	})
	rule := projectRule()
	rule.Condition = `Status == "Activated" && NOT(HasProject)`
	schema := issueSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{APIName: "Status", Type: constants.FieldTypeString})

	if err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy", "Status": "Activated"}, schema, []*models.ValidationRule{rule}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord error = %v, want nil", err)
	}
	if calls != 1 {
		t.Fatalf("record-set resolver calls = %d, want 1 when the binding changes the outcome", calls)
	}
}

func TestValidationRuleRecordSetBindingSkipsQueryButStillRejectsWhenOutcomeIsIndependentTrue(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	calls := 0
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		calls++
		return false, nil
	})
	rule := projectRule()
	rule.Condition = `Status == "Draft" || HasProject`
	schema := issueSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{APIName: "Status", Type: constants.FieldTypeString})

	err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy", "Status": "Draft"}, schema, []*models.ValidationRule{rule}, nil, nil)
	if err == nil || !strings.Contains(err.Error(), rule.ErrorMessage) {
		t.Fatalf("ValidateRecord error = %v, want validation-rule rejection", err)
	}
	if calls != 0 {
		t.Fatalf("record-set resolver calls = %d, want 0 for a true binding-independent guard", calls)
	}
}

func TestValidationRuleRecordSetBindingIndependentTrueCollectsAllFailuresAndDisplayFields(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	vs.SetCollectAllValidationRules(true)
	calls := 0
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		calls++
		return false, nil
	})
	projectID := "ProjectId"
	subject := "Subject"
	independent := projectRule()
	independent.Name = "Draft_Requires_Project"
	independent.Condition = `Status == "Draft" || HasProject`
	independent.ErrorDisplayField = &projectID
	second := &models.ValidationRule{
		Name:              "Subject_Required",
		Active:            true,
		Condition:         `ISBLANK(Subject)`,
		ErrorMessage:      "Subject is required",
		ErrorDisplayField: &subject,
	}
	schema := issueSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{APIName: "Status", Type: constants.FieldTypeString})

	err := vs.ValidateRecord(
		context.Background(),
		models.SObject{"Status": "Draft"},
		schema,
		[]*models.ValidationRule{independent, second},
		nil,
		nil,
	)
	failures, ok := apperrors.AsValidationRuleFailures(err)
	if !ok {
		t.Fatalf("ValidateRecord error = %T %v, want validation-rule failures", err, err)
	}
	want := []apperrors.ValidationRuleFailure{
		{RuleName: "Draft_Requires_Project", Message: "Project is required", Field: "ProjectId"},
		{RuleName: "Subject_Required", Message: "Subject is required", Field: "Subject"},
	}
	if !reflect.DeepEqual(failures, want) {
		t.Fatalf("ValidateRecord failures = %#v, want %#v", failures, want)
	}
	if calls != 0 {
		t.Fatalf("record-set resolver calls = %d, want 0 for a true binding-independent guard", calls)
	}
}

func TestValidationRuleRecordSetBindingRelationshipConditionKeepsCompatibilityPath(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	calls := 0
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		calls++
		return false, nil
	})
	rule := projectRule()
	rule.Condition = `Parent.Status == "Active" && HasProject`
	relationName := "Parent"
	schema := issueSchema()
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName: "ParentId", Type: constants.FieldTypeReference,
		ReferenceTo: []string{"Project"}, RelationshipName: &relationName,
	})
	vs.SetFormulaRelationResolver(func(context.Context, string, models.SObject, string) (models.SObject, error) {
		return models.SObject{"Status": "Inactive"}, nil
	})

	if err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy", "ParentId": "P1"}, schema, []*models.ValidationRule{rule}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord error = %v, want nil", err)
	}
	if calls != 1 {
		t.Fatalf("record-set resolver calls = %d, want compatibility-path query for relationship condition", calls)
	}
}

func TestValidationRuleRecordSetBindingDoesNotLeakAcrossRules(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	calls := 0
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		calls++
		return true, nil
	})

	ruleWithBinding := projectRule()
	ruleWithBinding.Name = "Project_Binding_Source"
	ruleWithBinding.Condition = "NOT(HasProject)"
	ruleWithoutBinding := &models.ValidationRule{
		Name:         "Project_Binding_Missing",
		Active:       true,
		Condition:    "HasProject",
		ErrorMessage: "leaked binding",
	}

	err := vs.ValidateRecord(
		context.Background(),
		models.SObject{"Subject": "Deploy", "ProjectId": "P1"},
		issueSchema(),
		[]*models.ValidationRule{ruleWithBinding, ruleWithoutBinding},
		nil,
		nil,
	)
	if err == nil {
		t.Fatalf("ValidateRecord error = nil, want missing binding failure")
	}
	var ruleErr *apperrors.ValidationRuleError
	if errors.As(err, &ruleErr) {
		t.Fatalf("ValidateRecord returned ValidationRuleError %q, want fail-closed evaluation error", ruleErr.Message)
	}
	if !strings.Contains(err.Error(), "Validation rule 'Project_Binding_Missing' failed to evaluate") {
		t.Fatalf("ValidateRecord error = %v, want Project_Binding_Missing evaluation failure", err)
	}
	if calls != 1 {
		t.Fatalf("record-set resolver calls = %d, want 1", calls)
	}
}

func TestValidationRuleRecordSetBindingInactiveRuleIsNoop(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	called := false
	vs.SetRecordSetBindingResolver(func(context.Context, *models.ObjectMetadata, models.SObject, *models.ValidationRule, models.ValidationRuleRecordSetBinding, map[string]interface{}) (bool, error) {
		called = true
		return false, nil
	})
	rule := projectRule()
	rule.Active = false

	if err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy"}, issueSchema(), []*models.ValidationRule{rule}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord inactive rule error = %v, want nil", err)
	}
	if called {
		t.Fatalf("record-set resolver was called for an inactive rule")
	}
}

func TestValidationRuleRecordSetBindingGreenfieldNoBindingsIsNoop(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	rule := &models.ValidationRule{
		Name:         "Subject_Not_Blank",
		Active:       true,
		Condition:    "ISBLANK(Subject)",
		ErrorMessage: "Subject is required",
	}

	if err := vs.ValidateRecord(context.Background(), models.SObject{"Subject": "Deploy"}, issueSchema(), []*models.ValidationRule{rule}, nil, nil); err != nil {
		t.Fatalf("ValidateRecord no-binding rule error = %v, want nil", err)
	}
}

func TestResolveRecordSetPathValueKeepsLegacyRecordTypeDeveloperNameCompatibility(t *testing.T) {
	recordTypeName := "Contract Only (APAC)"
	recordTypeDeveloperName := "Contract_Only_APAC"
	recordTypeRelationship := constants.ObjectRecordType
	schema := &models.ObjectMetadata{
		APIName: "Order",
		Fields: []models.FieldMetadata{{
			APIName:          constants.FieldRecordTypeID,
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{constants.ObjectRecordType},
			RelationshipName: &recordTypeRelationship,
		}},
	}
	record := models.SObject{constants.FieldRecordTypeID: "012000000000001AAA"}
	rule := &models.ValidationRule{IsCustomized: true}
	relationResolve := func(context.Context, string, models.SObject, string) (models.SObject, error) {
		return models.SObject{
			constants.FieldID:   "012000000000001AAA",
			constants.FieldName: recordTypeName,
			"DeveloperName":     recordTypeDeveloperName,
		}, nil
	}

	value, err := resolveRecordSetPathValue(context.Background(), schema, record, rule, "RecordType.DeveloperName", relationResolve, nil)
	if err != nil {
		t.Fatalf("resolveRecordSetPathValue error = %v, want nil", err)
	}
	if value != recordTypeName {
		t.Fatalf("resolveRecordSetPathValue = %#v, want legacy label-shaped DeveloperName %q", value, recordTypeName)
	}
}

func TestRecordSetCandidateMatchesUsesCanonicalRecordTypeDeveloperNameForPackRule(t *testing.T) {
	recordTypeName := "Contract Only (APAC)"
	recordTypeDeveloperName := "Contract_Only_APAC"
	ownerPack := "salesforce-crm-standard"
	recordTypeRelationship := constants.ObjectRecordType
	candidateSchema := &models.ObjectMetadata{
		APIName: "Order",
		Fields: []models.FieldMetadata{{
			APIName:          constants.FieldRecordTypeID,
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{constants.ObjectRecordType},
			RelationshipName: &recordTypeRelationship,
		}},
	}
	candidate := models.SObject{constants.FieldRecordTypeID: "012000000000001AAA"}
	rule := &models.ValidationRule{OwnerPack: &ownerPack}
	relationResolve := func(context.Context, string, models.SObject, string) (models.SObject, error) {
		return models.SObject{
			constants.FieldID:   "012000000000001AAA",
			constants.FieldName: recordTypeName,
			"DeveloperName":     recordTypeDeveloperName,
		}, nil
	}
	filter := resolvedRecordSetFilter{
		filter: models.ValidationRuleRecordSetFilter{Field: "RecordType.DeveloperName"},
		op:     "equals",
		value:  recordTypeDeveloperName,
		field:  nil,
	}

	matched, err := recordSetCandidateMatches(
		context.Background(),
		candidateSchema,
		candidate,
		rule,
		"1",
		[]resolvedRecordSetFilter{filter},
		relationResolve,
		nil,
		formula.NewEngine(),
	)
	if err != nil {
		t.Fatalf("recordSetCandidateMatches error = %v, want nil", err)
	}
	if !matched {
		t.Fatal("recordSetCandidateMatches = false, want canonical DeveloperName match for pack-owned rule")
	}
}

func TestRecordSetResultExistsScansCandidatesBeforeOverLimitGuard(t *testing.T) {
	binding := bindingWithFilter("HasProject")
	filter := resolvedRecordSetFilter{
		filter: binding.Filters[0],
		op:     "equals",
		value:  "P1",
		field:  &models.FieldMetadata{APIName: "Id", Type: constants.FieldTypeString},
	}

	ok, err := recordSetResultExists(
		context.Background(),
		projectSchema(),
		&models.QueryResult{Done: false, Records: []models.SObject{{"Id": "P1"}}},
		nil,
		binding,
		[]resolvedRecordSetFilter{filter},
		nil,
		nil,
		formula.NewEngine(),
	)
	if err != nil || !ok {
		t.Fatalf("recordSetResultExists matching partial page = (%v, %v), want (true, nil)", ok, err)
	}

	ok, err = recordSetResultExists(
		context.Background(),
		projectSchema(),
		&models.QueryResult{Done: false, Records: []models.SObject{{"Id": "P2"}}},
		nil,
		binding,
		[]resolvedRecordSetFilter{filter},
		nil,
		nil,
		formula.NewEngine(),
	)
	if err == nil || !strings.Contains(err.Error(), "exceeded the 500-row safety limit") {
		t.Fatalf("recordSetResultExists unmatched partial page = (%v, %v), want over-limit error", ok, err)
	}
	if ok {
		t.Fatalf("recordSetResultExists unmatched partial page ok = true, want false")
	}
}

func TestRecordSetDateEqualityNormalizesScannedTimeValues(t *testing.T) {
	field := &models.FieldMetadata{APIName: "DueDate", Type: constants.FieldTypeDate}
	dbDate := time.Date(2026, time.May, 12, 0, 0, 0, 0, time.FixedZone("Local", 8*60*60))

	ok, err := compareRecordSetValues(dbDate, "equals", "2026-05-12", field)
	if err != nil || !ok {
		t.Fatalf("date equals = (%v, %v), want (true, nil)", ok, err)
	}
	ok, err = compareRecordSetValues(dbDate, "notEquals", "2026-05-12", field)
	if err != nil || ok {
		t.Fatalf("date notEquals = (%v, %v), want (false, nil)", ok, err)
	}
	ok, err = compareRecordSetValues(dbDate, "in", []interface{}{"2026-05-11", "2026-05-12"}, field)
	if err != nil || !ok {
		t.Fatalf("date in = (%v, %v), want (true, nil)", ok, err)
	}
}

func TestRecordSetDatetimePushdownPreservesTimestampPrecision(t *testing.T) {
	field := &models.FieldMetadata{APIName: "OccurredAt", Type: constants.FieldTypeDatetime}
	value := time.Date(2026, time.May, 12, 13, 45, 30, 123456000, time.UTC)

	expr := recordSetFilterPushdownExpr(resolvedRecordSetFilter{
		filter: models.ValidationRuleRecordSetFilter{Field: "OccurredAt"},
		op:     "equals",
		value:  value,
		field:  field,
	})
	want := `OccurredAt == "2026-05-12 13:45:30.123456"`
	if expr != want {
		t.Fatalf("recordSetFilterPushdownExpr = %q, want %q", expr, want)
	}
}

func TestValidationRuleRecordSetBindingBasicValidation(t *testing.T) {
	vs := NewValidationService(formula.NewEngine())
	cases := []struct {
		name    string
		binding models.ValidationRuleRecordSetBinding
		want    string
	}{
		{
			name:    "missing name",
			binding: bindingWithFilter(""),
			want:    "name is required",
		},
		{
			name: "unsupported operator",
			binding: func() models.ValidationRuleRecordSetBinding {
				b := bindingWithFilter("HasProject")
				b.Filters[0].Operator = "around"
				return b
			}(),
			want: "unsupported operator",
		},
		{
			name: "malformed filter logic",
			binding: func() models.ValidationRuleRecordSetBinding {
				b := bindingWithFilter("HasProject")
				b.Filters = append(b.Filters, b.Filters[0])
				b.FilterLogic = "1 AND OR 2"
				return b
			}(),
			want: "malformed filterLogic",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := vs.validateRecordSetBindingBasic(issueSchema(), tc.binding, map[string]struct{}{})
			if err == nil || !strings.Contains(err.Error(), tc.want) {
				t.Fatalf("validateRecordSetBindingBasic error = %v, want containing %q", err, tc.want)
			}
		})
	}
}

func TestValidationRuleRecordSetBindingPathValidation(t *testing.T) {
	root := issueSchema()
	candidate := projectSchema()
	lookup := func(objectAPIName string) *models.ObjectMetadata {
		switch objectAPIName {
		case "Issue":
			return root
		case "Project":
			return candidate
		default:
			return nil
		}
	}

	t.Run("unknown candidate field", func(t *testing.T) {
		binding := bindingWithFilter("HasProject")
		binding.Filters[0].Field = "Missing"
		err := validateRecordSetBindingSchemas(root, candidate, binding, lookup)
		if err == nil || !strings.Contains(err.Error(), "unknown field") {
			t.Fatalf("validateRecordSetBindingSchemas error = %v, want unknown field", err)
		}
	})

	t.Run("bad record path", func(t *testing.T) {
		binding := bindingWithFilter("HasProject")
		binding.Filters[0].ValueSource = "recordPath"
		binding.Filters[0].Value = "Missing"
		err := validateRecordSetBindingSchemas(root, candidate, binding, lookup)
		if err == nil || !strings.Contains(err.Error(), "recordPath") {
			t.Fatalf("validateRecordSetBindingSchemas error = %v, want bad recordPath", err)
		}
	})
}

func projectRule() *models.ValidationRule {
	return &models.ValidationRule{
		Name:         "Issue_Requires_Project",
		Active:       true,
		Condition:    "ISNEW() && NOT(HasProject)",
		ErrorMessage: "Project is required",
		RecordSetBindings: []models.ValidationRuleRecordSetBinding{
			bindingWithFilter("HasProject"),
		},
	}
}

func bindingWithFilter(name string) models.ValidationRuleRecordSetBinding {
	return models.ValidationRuleRecordSetBinding{
		Name:          name,
		Mode:          "exists",
		ObjectAPIName: "Project",
		Filters: []models.ValidationRuleRecordSetFilter{{
			Field:       "Id",
			Operator:    "equals",
			ValueSource: "recordPath",
			Value:       "ProjectId",
		}},
	}
}

func issueSchema() *models.ObjectMetadata {
	return &models.ObjectMetadata{
		APIName: "Issue",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString},
			{APIName: "Subject", Type: constants.FieldTypeString},
			{APIName: "ProjectId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Project"}},
		},
	}
}

func projectSchema() *models.ObjectMetadata {
	return &models.ObjectMetadata{
		APIName: "Project",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString},
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}
}

// --- #1747 regression: null-ordered crash guard + deterministically-false short-circuit ---

func nullOrderedFilter(field, op string) resolvedRecordSetFilter {
	return resolvedRecordSetFilter{filter: models.ValidationRuleRecordSetFilter{Field: field}, op: op, value: nil}
}

func manyEqFilters(n int) []resolvedRecordSetFilter {
	out := make([]resolvedRecordSetFilter, n)
	for i := range out {
		out[i] = resolvedRecordSetFilter{filter: models.ValidationRuleRecordSetFilter{Field: "F"}, op: "equals", value: "x"}
	}
	return out
}

func TestRecordSetFilterPushdownExprDropsNullOrdered(t *testing.T) {
	// An ordered comparison against nil must NOT render `field OP NULL` (the walker
	// rejects it -> the #1747 500); it drops to an empty leaf (over-fetch the recheck corrects).
	for _, op := range []string{"lessOrEqual", "greaterOrEqual", "lessThan", "greaterThan"} {
		if got := recordSetFilterPushdownExpr(nullOrderedFilter("EndDate", op)); got != "" {
			t.Errorf("ordered %q vs nil: pushdown = %q, want \"\" (dropped, not rendered)", op, got)
		}
	}
	// equality-vs-nil is unaffected (renders; walker supports IS NULL / IS NOT NULL).
	if got := recordSetFilterPushdownExpr(resolvedRecordSetFilter{filter: models.ValidationRuleRecordSetFilter{Field: "Status"}, op: "equals", value: nil}); got == "" {
		t.Errorf("equals vs nil: pushdown = \"\", want a rendered IS NULL predicate")
	}
	// a non-nil ordered comparand still renders.
	nonNil := resolvedRecordSetFilter{filter: models.ValidationRuleRecordSetFilter{Field: "EndDate"}, op: "greaterOrEqual", value: "2026-01-01", field: &models.FieldMetadata{APIName: "EndDate", Type: constants.FieldTypeDate}}
	if got := recordSetFilterPushdownExpr(nonNil); got == "" {
		t.Errorf("non-nil ordered: pushdown = \"\", want a rendered predicate")
	}
}

func TestRecordSetBindingDeterministicallyFalse(t *testing.T) {
	eng := formula.NewEngine()
	eq := func(field string) resolvedRecordSetFilter {
		return resolvedRecordSetFilter{filter: models.ValidationRuleRecordSetFilter{Field: field}, op: "equals", value: "x"}
	}
	cases := []struct {
		name        string
		filterLogic string
		filters     []resolvedRecordSetFilter
		want        bool
	}{
		// The #1747 repro: both date recordPaths resolve to nil, default-AND -> unsatisfiable.
		{"AND of two null-ordered (repro)", "", []resolvedRecordSetFilter{nullOrderedFilter("StartDate", "lessOrEqual"), nullOrderedFilter("EndDate", "greaterOrEqual")}, true},
		{"single null-ordered", "1", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual")}, true},
		{"AND of null-ordered + equals", "1 AND 2", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual"), eq("Status")}, true},
		// Satisfiable via the surviving disjunct -> must NOT short-circuit.
		{"OR of null-ordered + equals (satisfiable)", "1 OR 2", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual"), eq("Status")}, false},
		// No null-ordered leaf -> nothing to short-circuit.
		{"no null-ordered", "1 AND 2", []resolvedRecordSetFilter{eq("Status"), eq("Type")}, false},
		// Negation (NOT) makes the optimistic-true assignment unsound -> must NOT
		// short-circuit. `1 OR NOT 2` with leaf 1 nil-ordered is satisfiable by a candidate
		// where leaf 2 does not match.
		{"OR NOT (negation, satisfiable)", "1 OR NOT 2", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual"), eq("Status")}, false},
		{"NOT OR (negation, satisfiable)", "NOT 2 OR 1", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual"), eq("Status")}, false},
		// NOT of a nil-ordered no-match matches everything -> satisfiable -> no short-circuit.
		{"NOT of null-ordered", "NOT 1", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual")}, false},
		// Provably-false WITH negation: exact SAT must still short-circuit these.
		// `1 AND NOT 2` with leaf 1 nil-ordered -> false AND anything -> always false.
		{"AND NOT (provably false)", "1 AND NOT 2", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual"), eq("Status")}, true},
		// `1 OR (2 AND NOT 2)` with leaf 1 nil-ordered -> false OR (x AND NOT x) -> always false.
		{"OR contradiction (provably false)", "1 OR (2 AND NOT 2)", []resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual"), eq("Status")}, true},
		// Unreferenced filters must NOT count against the satisfiability bound: filterLogic
		// references only filter 1 (nil-ordered -> false), so the binding is provably false
		// even with many irrelevant extra filters defined.
		{"referenced-1 null-ordered + 24 unused", "1", append([]resolvedRecordSetFilter{nullOrderedFilter("EndDate", "greaterOrEqual")}, manyEqFilters(24)...), true},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := recordSetBindingDeterministicallyFalse(c.filterLogic, c.filters, eng); got != c.want {
				t.Errorf("recordSetBindingDeterministicallyFalse = %v, want %v", got, c.want)
			}
		})
	}
}
