package automation

import (
	"context"
	"errors"
	"strings"
	"testing"

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

// Regression coverage for #3913: approval entry/step criteria referencing a
// virtual (active-mode) formula field must see the projected value. The
// internal QueryWithFilter record does not carry query-time formula fields,
// so before the overlay the criterion compared nil and never matched — and
// with the Salesforce default ifCriteriaNotMet=ApproveRecord every
// formula-gated step silently auto-approved.
//
// The overlay reuses the canonical #1362 projection seam
// (PersistenceProvider.RecomputeActiveFormulaProjection); these tests pin
// both scale conventions explicitly: criteria items compare in presentation
// form (stored 92 vs authored "90"), while criteria formulas evaluate in
// formula space (92 normalized to 0.92 by percent semantics).

type formulaProjectionQueryStub struct {
	ports.QueryService
	record models.SObject
}

func (s *formulaProjectionQueryStub) QueryWithFilter(_ context.Context, _ string, _ string, _ *models.UserSession, _ string, _ string, _ int) (*models.QueryResult, error) {
	clone := models.SObject{}
	for k, v := range s.record {
		clone[k] = v
	}
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{clone}}, nil
}

// QueryWithSpec serves the criteria evaluator's field-capped SystemContext
// read (#3945). The stub asserts the elevation invariant: the criteria read
// must arrive with SystemContext (a user-context read here would mean the
// evaluator regressed to submitter FLS).
func (s *formulaProjectionQueryStub) QueryWithSpec(ctx context.Context, _ queryspec.QuerySpec, _ *models.UserSession) (*models.QueryResult, error) {
	if constants.GetSecurityContext(ctx) < constants.SystemContext {
		return nil, errors.New("criteria read arrived without SystemContext")
	}
	clone := models.SObject{}
	for k, v := range s.record {
		clone[k] = v
	}
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{clone}}, nil
}

type formulaProjectionPersistenceStub struct {
	PersistenceProvider
	snapshot models.SObject
	err      error
	calls    int
}

func (s *formulaProjectionPersistenceStub) RecomputeActiveFormulaProjection(_ context.Context, _ *models.UserSession, _, _ string, _ []string) (models.SObject, error) {
	s.calls++
	if s.err != nil {
		return nil, s.err
	}
	return s.snapshot, nil
}

func (s *formulaProjectionPersistenceStub) GetRaw(_ context.Context, _ string, id string) (models.SObject, error) {
	return models.SObject{"Id": id}, nil
}

func (s *formulaProjectionPersistenceStub) ReadSelectedSystemRecord(_ context.Context, _ string, id string, fields []string) (models.SObject, error) {
	record := make(models.SObject, len(fields))
	for _, field := range fields {
		if field == constants.FieldID {
			record[field] = id
		}
	}
	return record, nil
}

func formulaProjectionSchema() SchemaLookup {
	active := "active"
	expr := "CanaryA / CanaryB"
	marginField := &models.FieldMetadata{
		APIName:     "Margin_Rate__c",
		Type:        models.FieldType(constants.FieldTypePercent),
		Formula:     &expr,
		FormulaMode: &active,
	}
	return criteriaSchemaStub{schemas: map[string]*models.ObjectMetadata{
		"Widget": {
			APIName:   "Widget",
			Fields:    []models.FieldMetadata{*marginField},
			FieldsMap: map[string]*models.FieldMetadata{"margin_rate__c": marginField},
		},
	}}
}

func newFormulaProjectionService(t *testing.T, persistence PersistenceProvider, query ports.QueryService) *ApprovalService {
	t.Helper()
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()
	return svc
}

func formulaProjectionTestUser() *models.UserSession {
	return &models.UserSession{ID: "user-1"}
}

type candidateFormulaProjectionQueryStub struct {
	ports.QueryService
	definitions     []models.SObject
	systemReadCalls int
}

func (s *candidateFormulaProjectionQueryStub) QueryWithFilter(_ context.Context, objectName, _ string, _ *models.UserSession, _ string, _ string, _ int) (*models.QueryResult, error) {
	if objectName != constants.TableProcessDefinition {
		return nil, errors.New("unexpected QueryWithFilter object: " + objectName)
	}
	return &models.QueryResult{TotalSize: len(s.definitions), Records: s.definitions}, nil
}

func (s *candidateFormulaProjectionQueryStub) QueryWithSpec(ctx context.Context, req queryspec.QuerySpec, _ *models.UserSession) (*models.QueryResult, error) {
	if constants.GetSecurityContext(ctx) < constants.SystemContext {
		return nil, errors.New("criteria read arrived without SystemContext")
	}
	if req.ObjectAPIName != "Widget" {
		return nil, errors.New("unexpected QueryWithSpec object: " + req.ObjectAPIName)
	}
	s.systemReadCalls++
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{{
		constants.FieldID: "rec-1",
		"Margin_Rate__c":  nil,
	}}}, nil
}

func TestResolveSubmitEligibilityCachesRootFormulaProjectionAcrossCandidates(t *testing.T) {
	entry := func(threshold string) string {
		return `{"criteriaItems":[{"field":"Margin_Rate__c","operation":"greaterOrEqual","value":"` + threshold + `"}]}`
	}
	query := &candidateFormulaProjectionQueryStub{definitions: []models.SObject{
		{constants.FieldID: "pd-first", "EntryFilter": entry("100")},
		{constants.FieldID: "pd-second", "EntryFilter": entry("95")},
		{constants.FieldID: "pd-match", "EntryFilter": entry("90")},
	}}
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{
		constants.FieldID: "rec-1",
		"Margin_Rate__c":  92,
	}}
	svc := newFormulaProjectionService(t, persistence, query)

	eligibility, err := svc.resolveSubmitEligibilityAgainstRecord(
		t.Context(), SubmitRequest{ObjectAPIName: "Widget", RecordID: "rec-1"},
		"rec-1", "Widget", models.SObject{constants.FieldID: "rec-1"}, formulaProjectionTestUser(),
	)
	if err != nil {
		t.Fatalf("resolveSubmitEligibilityAgainstRecord() error = %v", err)
	}
	if eligibility.ProcessDefinitionID != "pd-match" {
		t.Fatalf("ProcessDefinitionID = %q, want pd-match", eligibility.ProcessDefinitionID)
	}
	if query.systemReadCalls != 1 {
		t.Fatalf("criteria system reads = %d, want 1 across three candidates", query.systemReadCalls)
	}
	if persistence.calls != 1 {
		t.Fatalf("formula projections = %d, want 1 across three candidates", persistence.calls)
	}
}

func TestRuntimeApprovalLockedCriteriaRecomputesFormulaAbsentFromBaseRow(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{
		snapshot: models.SObject{constants.FieldID: "rec-1", "Margin_Rate__c": 0},
	}
	query := &formulaProjectionQueryStub{}
	svc := newFormulaProjectionService(t, persistence, query)
	ctx := withRuntimeApprovalLockedCriteriaTarget(
		t.Context(), "Widget", "rec-1",
		// GetLock reads the physical base table. Active formula columns do not
		// exist there, so this snapshot correctly omits Margin_Rate__c.
		models.SObject{constants.FieldID: "rec-1"},
	)
	matched, err := svc.matchEntryCriteriaAgainstRecord(
		ctx, "Margin_Rate__c lessOrEqual 5", "Widget", "rec-1",
		// The earlier user-visible view can carry a stale nil formula key. It
		// must not suppress recomputation against the locked transaction row.
		models.SObject{constants.FieldID: "rec-1", "Margin_Rate__c": nil},
		formulaProjectionTestUser(),
	)
	if err != nil {
		t.Fatal(err)
	}
	if !matched || persistence.calls != 1 || len(query.record) != 0 {
		t.Fatalf("matched=%v projection calls=%d query=%#v", matched, persistence.calls, query.record)
	}
}

func TestApprovalCriteriaRecomputesFormulaPresentAsNilInSystemRead(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{
		snapshot: models.SObject{constants.FieldID: "rec-1", "Margin_Rate__c": 0},
	}
	query := &formulaProjectionQueryStub{
		// Production-shaped field-capped reads can contain the virtual field key
		// with a nil value.  That is not a completed formula projection.
		record: models.SObject{constants.FieldID: "rec-1", "Margin_Rate__c": nil},
	}
	svc := newFormulaProjectionService(t, persistence, query)

	matched, err := svc.matchEntryCriteriaAgainstRecord(
		t.Context(), "Margin_Rate__c lessOrEqual 5", "Widget", "rec-1",
		models.SObject{constants.FieldID: "rec-1"}, formulaProjectionTestUser(),
	)
	if err != nil {
		t.Fatal(err)
	}
	if !matched || persistence.calls != 1 {
		t.Fatalf("matched=%v projection calls=%d, want true/1", matched, persistence.calls)
	}
}

func stepCriteriaDefNode(metadata string) (models.SObject, models.SObject) {
	def := models.SObject{constants.FieldID: "pd-1", "Metadata": metadata}
	node := models.SObject{constants.FieldID: "pn-1", "DeveloperName": "Margin_Gate", "StepOrder": 1}
	return def, node
}

// Presentation-scale convention: criteria items compare the projected stored
// value (92) against the authored threshold ("90").
func TestStepCriteriaItemsMatchProjectedVirtualFormulaField(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{"Margin_Rate__c": 92.0}}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}}
	svc := newFormulaProjectionService(t, persistence, query)

	def, node := stepCriteriaDefNode(`{
		"approvalStep": [{
			"name": "Margin_Gate",
			"stepCriteria": {"criteriaItems": [
				{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
			]},
			"ifCriteriaNotMet": "ApproveRecord"
		}]
	}`)

	matched, err := svc.stepCriteriaMatches(context.Background(), def, node, "Widget", "rec-1", formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("stepCriteriaMatches() error = %v", err)
	}
	if !matched {
		t.Fatalf("stepCriteriaMatches() = false, want true: projected 92 must satisfy >= 90 (pre-#3913 the nil lookup silently skipped the step)")
	}
	if persistence.calls != 1 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 1", persistence.calls)
	}
}

// The same shape must NOT match when the projected value is below the
// threshold — the overlay must not turn every criterion into a match.
func TestStepCriteriaItemsRespectProjectedValueBelowThreshold(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{"Margin_Rate__c": 42.0}}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1"}}
	svc := newFormulaProjectionService(t, persistence, query)

	def, node := stepCriteriaDefNode(`{
		"approvalStep": [{
			"name": "Margin_Gate",
			"stepCriteria": {"criteriaItems": [
				{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
			]},
			"ifCriteriaNotMet": "ApproveRecord"
		}]
	}`)

	matched, err := svc.stepCriteriaMatches(context.Background(), def, node, "Widget", "rec-1", formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("stepCriteriaMatches() error = %v", err)
	}
	if matched {
		t.Fatalf("stepCriteriaMatches() = true, want false for projected 42 vs >= 90")
	}
}

// Formula-space convention: a criteria FORMULA sees the percent field through
// the engine's normalization (stored 92 -> 0.92), so fraction-form thresholds
// authored per the SF convention compare correctly.
func TestStepCriteriaFormulaSeesProjectedPercentInFormulaSpace(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{"Margin_Rate__c": 92.0}}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1"}}
	svc := newFormulaProjectionService(t, persistence, query)

	def, node := stepCriteriaDefNode(`{
		"approvalStep": [{
			"name": "Margin_Gate",
			"stepCriteria": {"formula": "AND(Margin_Rate__c >= 0.9, Margin_Rate__c <= 1)"},
			"ifCriteriaNotMet": "ApproveRecord"
		}]
	}`)

	matched, err := svc.stepCriteriaMatches(context.Background(), def, node, "Widget", "rec-1", formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("stepCriteriaMatches() error = %v", err)
	}
	// The upper bound (<= 1) is what makes this discriminating: raw 92 fails
	// it, only the /100-normalized 0.92 satisfies both bounds.
	if !matched {
		t.Fatalf("stepCriteriaMatches() = false, want true: projected 92 must evaluate as 0.92 in formula space")
	}
}

type formulaProjectionMultiObjectQueryStub struct {
	ports.QueryService
	byObject map[string]models.SObject
}

func (s *formulaProjectionMultiObjectQueryStub) QueryWithFilter(_ context.Context, objectName string, _ string, _ *models.UserSession, _ string, _ string, _ int) (*models.QueryResult, error) {
	record, ok := s.byObject[objectName]
	if !ok {
		return &models.QueryResult{TotalSize: 0}, nil
	}
	clone := models.SObject{}
	for k, v := range record {
		clone[k] = v
	}
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{clone}}, nil
}

func (s *formulaProjectionMultiObjectQueryStub) QueryWithSpec(ctx context.Context, spec queryspec.QuerySpec, _ *models.UserSession) (*models.QueryResult, error) {
	if constants.GetSecurityContext(ctx) < constants.SystemContext {
		return nil, errors.New("criteria read arrived without SystemContext")
	}
	record, ok := s.byObject[spec.ObjectAPIName]
	if !ok {
		return &models.QueryResult{TotalSize: 0}, nil
	}
	clone := models.SObject{}
	for k, v := range record {
		clone[k] = v
	}
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{clone}}, nil
}

// Cross-object criteria hop: a criterion on a PARENT object's virtual formula
// field must see the projected parent value too — the parent load in
// resolveCrossObjectValue previously bypassed the overlay entirely, keeping
// the #3913 silent-auto-approve shape alive one hop up.
func TestStepCriteriaItemsProjectCrossObjectVirtualFormulaField(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{"Margin_Rate__c": 92.0}}
	query := &formulaProjectionMultiObjectQueryStub{byObject: map[string]models.SObject{
		"Gadget": {constants.FieldID: "child-1", "WidgetId": "parent-1"},
		"Widget": {constants.FieldID: "parent-1", "Name": "W-1"},
	}}
	active := "active"
	expr := "CanaryA / CanaryB"
	marginField := &models.FieldMetadata{
		APIName: "Margin_Rate__c", Type: models.FieldType(constants.FieldTypePercent),
		Formula: &expr, FormulaMode: &active,
	}
	plainField := &models.FieldMetadata{APIName: "WidgetId", Type: models.FieldType(constants.FieldTypeReference)}
	schema := criteriaSchemaStub{schemas: map[string]*models.ObjectMetadata{
		"Widget": {
			APIName:   "Widget",
			Fields:    []models.FieldMetadata{*marginField},
			FieldsMap: map[string]*models.FieldMetadata{"margin_rate__c": marginField},
		},
		"Gadget": {
			APIName:   "Gadget",
			Fields:    []models.FieldMetadata{*plainField},
			FieldsMap: map[string]*models.FieldMetadata{"widgetid": plainField},
		},
	}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, schema, logging.Default)
	svc.formulaEngine = formula.NewEngine()

	def, node := stepCriteriaDefNode(`{
		"approvalStep": [{
			"name": "Margin_Gate",
			"stepCriteria": {"criteriaItems": [
				{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
			]},
			"ifCriteriaNotMet": "ApproveRecord"
		}]
	}`)

	matched, err := svc.stepCriteriaMatches(context.Background(), def, node, "Gadget", "child-1", formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("stepCriteriaMatches() error = %v", err)
	}
	if !matched {
		t.Fatalf("stepCriteriaMatches() = false, want true: parent-hop projected 92 must satisfy >= 90")
	}
	if persistence.calls == 0 {
		t.Fatal("RecomputeActiveFormulaProjection never called for the parent record")
	}
}

// A parent projection ERROR must not become a plain non-match (which
// ifCriteriaNotMet=ApproveRecord would turn into a silent auto-approve):
// step criteria treat evaluation errors as MATCH, so the gate engages.
func TestStepCriteriaCrossObjectProjectionErrorTreatsStepAsMatched(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionMultiObjectQueryStub{byObject: map[string]models.SObject{
		"Gadget": {constants.FieldID: "child-1", "WidgetId": "parent-1"},
		"Widget": {constants.FieldID: "parent-1"},
	}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, crossObjectProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	def, node := stepCriteriaDefNode(`{
		"approvalStep": [{
			"name": "Margin_Gate",
			"stepCriteria": {"criteriaItems": [
				{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
			]},
			"ifCriteriaNotMet": "ApproveRecord"
		}]
	}`)

	matched, err := svc.stepCriteriaMatches(context.Background(), def, node, "Gadget", "child-1", formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("stepCriteriaMatches() error = %v", err)
	}
	if !matched {
		t.Fatal("stepCriteriaMatches() = false on projection error, want true (fail-safe: the gate must engage, not silently auto-approve)")
	}
}

// The entry-criteria posture is fail-closed: the same projection error fails
// the evaluation instead of silently deciding process eligibility.
func TestEntryCriteriaCrossObjectProjectionErrorFailsClosed(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionMultiObjectQueryStub{byObject: map[string]models.SObject{
		"Gadget": {constants.FieldID: "child-1", "WidgetId": "parent-1"},
		"Widget": {constants.FieldID: "parent-1"},
	}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, crossObjectProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "child-1", "WidgetId": "parent-1"}
	entry := `{"criteriaItems": [{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}]}`
	_, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Gadget", "child-1", record, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("matchEntryCriteriaAgainstRecord() error = nil, want projection failure to propagate")
	}
}

// Top-level submit posture (review round 2 adversarial): with a SINGLE active
// process whose entry criteria reference a formula field, a projection error
// must fail the submit resolution — the old leniency ("single process +
// evaluation error → allow") would have flipped the propagated error class
// from submit-rejected to submit-proceeds.
func TestResolveSubmitEligibilitySingleProcessProjectionErrorFailsClosed(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	entry := `{"criteriaItems": [{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}]}`
	query := &submitEligibilityQueryStub{
		target: models.SObject{constants.FieldID: "rec-1"},
		processDefs: []models.SObject{{
			constants.FieldID: "pd-1",
			"TableEnumOrId":   "Widget",
			"EntryFilter":     entry,
		}},
	}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	_, err := svc.ResolveSubmitEligibility(context.Background(),
		SubmitRequest{ObjectAPIName: "Widget", RecordID: "rec-1"}, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("ResolveSubmitEligibility() error = nil, want fail-closed on entry-criteria projection error (single active process)")
	}
	if !strings.Contains(err.Error(), "entry criteria") {
		t.Fatalf("ResolveSubmitEligibility() error = %v, want entry-criteria evaluation failure", err)
	}
}

// First-match-wins ordering (review round 6 adversarial): a record-unavailable
// failure on a HIGHER-priority process must abort resolution — a later blank
// or matching process must not be selected, because the skipped process might
// have been the true first match.
func TestResolveSubmitEligibilityUnavailableHigherPriorityAbortsBeforeLaterBlank(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	entry := `{"criteriaItems": [{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}]}`
	query := &submitEligibilityQueryStub{
		target: models.SObject{constants.FieldID: "rec-1"},
		processDefs: []models.SObject{
			{constants.FieldID: "pd-1", "TableEnumOrId": "Widget", "EntryFilter": entry},
			{constants.FieldID: "pd-2", "TableEnumOrId": "Widget", "EntryFilter": ""},
		},
	}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	_, err := svc.ResolveSubmitEligibility(context.Background(),
		SubmitRequest{ObjectAPIName: "Widget", RecordID: "rec-1"}, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("ResolveSubmitEligibility() error = nil, want abort: later blank process must not be selected past an unevaluable higher-priority process")
	}
	if !strings.Contains(err.Error(), "entry criteria") {
		t.Fatalf("ResolveSubmitEligibility() error = %v, want entry-criteria evaluation failure", err)
	}
}

type submitEligibilityQueryStub struct {
	ports.QueryService
	target      models.SObject
	processDefs []models.SObject
}

func (s *submitEligibilityQueryStub) QueryWithFilter(_ context.Context, objectName string, _ string, _ *models.UserSession, _ string, _ string, _ int) (*models.QueryResult, error) {
	if objectName == constants.TableProcessDefinition {
		return &models.QueryResult{TotalSize: len(s.processDefs), Records: s.processDefs}, nil
	}
	if objectName == constants.TableProcessInstance {
		return &models.QueryResult{TotalSize: 0}, nil
	}
	clone := models.SObject{}
	for k, v := range s.target {
		clone[k] = v
	}
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{clone}}, nil
}

func (s *submitEligibilityQueryStub) QueryWithSpec(ctx context.Context, _ queryspec.QuerySpec, _ *models.UserSession) (*models.QueryResult, error) {
	if constants.GetSecurityContext(ctx) < constants.SystemContext {
		return nil, errors.New("criteria read arrived without SystemContext")
	}
	clone := models.SObject{}
	for k, v := range s.target {
		clone[k] = v
	}
	return &models.QueryResult{TotalSize: 1, Records: []models.SObject{clone}}, nil
}

func crossObjectProjectionSchema() SchemaLookup {
	active := "active"
	expr := "CanaryA / CanaryB"
	marginField := &models.FieldMetadata{
		APIName: "Margin_Rate__c", Type: models.FieldType(constants.FieldTypePercent),
		Formula: &expr, FormulaMode: &active,
	}
	plainField := &models.FieldMetadata{APIName: "WidgetId", Type: models.FieldType(constants.FieldTypeReference)}
	return criteriaSchemaStub{schemas: map[string]*models.ObjectMetadata{
		"Widget": {
			APIName:   "Widget",
			Fields:    []models.FieldMetadata{*marginField},
			FieldsMap: map[string]*models.FieldMetadata{"margin_rate__c": marginField},
		},
		"Gadget": {
			APIName:   "Gadget",
			Fields:    []models.FieldMetadata{*plainField},
			FieldsMap: map[string]*models.FieldMetadata{"widgetid": plainField},
		},
	}}
}

func TestOverlayActiveFormulaProjectionSkipsSchemasWithoutActiveFields(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{}}
	plainField := &models.FieldMetadata{APIName: "Name", Type: models.FieldType(constants.FieldTypeString)}
	schema := criteriaSchemaStub{schemas: map[string]*models.ObjectMetadata{
		"Widget": {
			APIName:   "Widget",
			Fields:    []models.FieldMetadata{*plainField},
			FieldsMap: map[string]*models.FieldMetadata{"name": plainField},
		},
	}}
	svc := NewApprovalService(persistence, nil, nil, nil, nil, nil, schema, logging.Default)

	record := models.SObject{constants.FieldID: "rec-1"}
	out, err := svc.overlayActiveFormulaProjection(context.Background(), "Widget", "rec-1", record, nil, formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("overlayActiveFormulaProjection() error = %v", err)
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 for schema without active fields", persistence.calls)
	}
	if len(out) != 1 {
		t.Fatalf("record mutated for schema without active fields: %#v", out)
	}
}

// Projection failure must fail closed where criteria are actually evaluated
// on a formula field: root-object entry criteria items propagate the error
// instead of silently degrading to the pre-#3913 nil comparison.
func TestEntryCriteriaRootFormulaProjectionErrorFailsClosed(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1"}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)

	record := models.SObject{constants.FieldID: "rec-1"}
	entry := `{"criteriaItems": [{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}]}`
	_, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("matchEntryCriteriaAgainstRecord() error = nil, want projection failure to propagate")
	}
	if !errors.Is(err, errCriteriaRecordUnavailable) {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v, want errCriteriaRecordUnavailable class", err)
	}
}

// Projection is deferred until criteria actually need it: a submit whose
// entry criteria are blank must not touch the projection port at all, so an
// unrelated formula-view failure cannot reject it (review round 2).
func TestEntryCriteriaBlankSkipsProjectionEntirely(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	svc := NewApprovalService(persistence, nil, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)

	record := models.SObject{constants.FieldID: "rec-1"}
	matched, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), "", "Widget", "rec-1", record, formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v, want nil for blank criteria despite failing projection port", err)
	}
	if !matched {
		t.Fatal("matchEntryCriteriaAgainstRecord() = false, want true for blank criteria")
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 for blank criteria", persistence.calls)
	}
}

// A criteria FORMULA that references only physical fields must not touch the
// projection port even when the schema has active formula fields elsewhere
// (review round 3: projection is scoped to the formula's actual references).
func TestEntryCriteriaFormulaOnPhysicalFieldsSkipsProjection(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}
	entry := `{"formula": "Name == \"W-1\""}`
	matched, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v, want nil for physical-only formula despite failing projection port", err)
	}
	if !matched {
		t.Fatal("matchEntryCriteriaAgainstRecord() = false, want true")
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 for physical-only formula", persistence.calls)
	}
}

// A cross-object criterion on a PHYSICAL parent field must not depend on
// formula-view availability (review round 3).
func TestStepCriteriaCrossObjectPhysicalFieldSkipsProjection(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionMultiObjectQueryStub{byObject: map[string]models.SObject{
		"Gadget": {constants.FieldID: "child-1", "WidgetId": "parent-1"},
		"Widget": {constants.FieldID: "parent-1", "Name": "W-1"},
	}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, crossObjectProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	def, node := stepCriteriaDefNode(`{
		"approvalStep": [{
			"name": "Margin_Gate",
			"stepCriteria": {"criteriaItems": [
				{"field": "Widget.Name", "operation": "equals", "value": "W-1"}
			]},
			"ifCriteriaNotMet": "ApproveRecord"
		}]
	}`)

	matched, err := svc.stepCriteriaMatches(context.Background(), def, node, "Gadget", "child-1", formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("stepCriteriaMatches() error = %v", err)
	}
	if !matched {
		t.Fatal("stepCriteriaMatches() = false, want true for physical parent field match")
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 for physical parent field", persistence.calls)
	}
}

// A booleanFilter that statically never references the formula item must not
// widen the projection dependency: filter "1" uses only the physical item,
// so the unused formula item triggers no projection (review round 5).
func TestEntryCriteriaBooleanFilterUnusedFormulaItemSkipsProjection(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}
	entry := `{
		"criteriaItems": [
			{"field": "Widget.Name", "operation": "equals", "value": "W-1"},
			{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
		],
		"booleanFilter": "1"
	}`
	matched, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v, want nil: filter-unused formula item must not require projection", err)
	}
	if !matched {
		t.Fatal("matchEntryCriteriaAgainstRecord() = false, want true via filter \"1\" on the physical item")
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 when the filter never references the formula item", persistence.calls)
	}
}

// A malformed filter (no parseable indices) cannot evaluate any item, so it
// must project NOTHING and fail as a criteria-CONTENT error under the
// preserved leniency — never as record-unavailable (review round 6: the
// all-items fallback wrongly escalated doomed evaluations into the
// fail-closed class when the projection port was down).
func TestEntryCriteriaMalformedFilterStaysContentErrorWithoutProjection(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	svc := NewApprovalService(persistence, nil, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}
	entry := `{
		"criteriaItems": [
			{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
		],
		"booleanFilter": "not a valid criteria expression"
	}`
	_, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("matchEntryCriteriaAgainstRecord() error = nil, want content-error from malformed filter")
	}
	if errors.Is(err, errCriteriaRecordUnavailable) {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v, want criteria-CONTENT class, not record-unavailable", err)
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 for a filter that evaluates nothing", persistence.calls)
	}
}

// A digit embedded in a malformed token ("foo1") must not count as a
// reference: the gating parse mirrors the evaluator's grammar, so the whole
// filter is invalid → zero projection → content-error class (round 7).
func TestEntryCriteriaDigitInMalformedTokenStaysContentError(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	svc := NewApprovalService(persistence, nil, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "rec-1"}
	entry := `{
		"criteriaItems": [
			{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
		],
		"booleanFilter": "foo1"
	}`
	_, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("matchEntryCriteriaAgainstRecord() error = nil, want content-error from malformed filter")
	}
	if errors.Is(err, errCriteriaRecordUnavailable) {
		t.Fatalf("error = %v, want criteria-CONTENT class for filter \"foo1\"", err)
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0: digit inside a malformed token is not a reference", persistence.calls)
	}
}

// A (nil, nil) projection snapshot is a comparison-basis failure, not a
// success: the needed formula value is still absent, and treating it as
// success would fall back to the silent nil compare (round 7 adversarial).
func TestEntryCriteriaNilSnapshotClassifiesRecordUnavailable(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: nil}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1"}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "rec-1"}
	entry := `{"criteriaItems": [{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}]}`
	_, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err == nil {
		t.Fatal("matchEntryCriteriaAgainstRecord() error = nil, want record-unavailable for nil projection snapshot")
	}
	if !errors.Is(err, errCriteriaRecordUnavailable) {
		t.Fatalf("error = %v, want errCriteriaRecordUnavailable class", err)
	}
	if persistence.calls != 1 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 1", persistence.calls)
	}
}

// Control: a filter that DOES reference the formula item still projects.
func TestEntryCriteriaBooleanFilterUsedFormulaItemProjects(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{snapshot: models.SObject{"Margin_Rate__c": 92.0}}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)
	svc.formulaEngine = formula.NewEngine()

	record := models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}
	entry := `{
		"criteriaItems": [
			{"field": "Widget.Name", "operation": "equals", "value": "W-1"},
			{"field": "Widget.Margin_Rate__c", "operation": "greaterOrEqual", "value": "90"}
		],
		"booleanFilter": "1 AND 2"
	}`
	matched, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v", err)
	}
	if !matched {
		t.Fatal("matchEntryCriteriaAgainstRecord() = false, want true (projected 92 >= 90 via filter item 2)")
	}
	if persistence.calls != 1 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 1", persistence.calls)
	}
}

// Criteria on plain physical fields must not trigger projection either, even
// when the schema has active formula fields elsewhere.
func TestEntryCriteriaOnPhysicalFieldSkipsProjection(t *testing.T) {
	persistence := &formulaProjectionPersistenceStub{err: context.DeadlineExceeded}
	query := &formulaProjectionQueryStub{record: models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}}
	svc := NewApprovalService(persistence, query, nil, nil, nil, nil, formulaProjectionSchema(), logging.Default)

	record := models.SObject{constants.FieldID: "rec-1", "Name": "W-1"}
	entry := `{"criteriaItems": [{"field": "Widget.Name", "operation": "equals", "value": "W-1"}]}`
	matched, err := svc.matchEntryCriteriaAgainstRecord(context.Background(), entry, "Widget", "rec-1", record, formulaProjectionTestUser())
	if err != nil {
		t.Fatalf("matchEntryCriteriaAgainstRecord() error = %v, want nil despite failing projection port", err)
	}
	if !matched {
		t.Fatal("matchEntryCriteriaAgainstRecord() = false, want true")
	}
	if persistence.calls != 0 {
		t.Fatalf("RecomputeActiveFormulaProjection calls = %d, want 0 for physical-field criteria", persistence.calls)
	}
}
