package formula

import (
	"errors"
	"testing"
)

func TestCollectCurrentObjectFieldReferences(t *testing.T) {
	got, err := CollectCurrentObjectFieldReferences(`Region__c == "APAC" || TEXT(Status__c) == "Open" || $Record.OwnerId == "005x" || $User.Name == "Kevin" || Parent__r.Name == "Acme"`)
	if err != nil {
		t.Fatalf("CollectCurrentObjectFieldReferences() error = %v", err)
	}

	want := []string{"Region__c", "Status__c", "OwnerId"}
	if len(got) != len(want) {
		t.Fatalf("CollectCurrentObjectFieldReferences() = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("CollectCurrentObjectFieldReferences()[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
		}
	}
}

// The engine exposes the current record under the runtime `record`/`prior`
// env names and the normalized prior-state roots in addition to __Record
// ($Record). A field accessed through any of them is a direct current-object
// field reference — omitting these forms silently strips the field from
// every reference-driven basis (projection need, read caps, rollup
// relevance), which is a correctness regression, not a conservative miss.
func TestCollectCurrentObjectFieldReferencesRecordNamespaceRoots(t *testing.T) {
	got, err := CollectCurrentObjectFieldReferences(`record.Status__c == "Escalated" || prior.Status__c == "Open" || $Record.Amount > 0 || record.Nested.Path == "x" || record["Indexed__c"] >= 90 || $Record["Bracketed__c"] == "y"`)
	if err != nil {
		t.Fatalf("CollectCurrentObjectFieldReferences() error = %v", err)
	}
	want := []string{"Status__c", "Amount", "Indexed__c", "Bracketed__c"}
	if len(got) != len(want) {
		t.Fatalf("CollectCurrentObjectFieldReferences() = %v, want %v (record./prior. roots collected, multi-segment chains excluded)", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("CollectCurrentObjectFieldReferences()[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
		}
	}
}

// PRIORVALUE/ISCHANGED lower to `_prior_<field>` identifiers; the collector
// must resolve those to the underlying field — the engine seeds
// `_prior_<field>` from the record's CURRENT values on no-prior paths, so a
// reference-driven read cap that carried the lowered name verbatim would
// silently drop the field (Codex round-3 on #3945). Engine magic sentinels
// are not fields.
func TestCollectCurrentObjectFieldReferencesPriorLoweredIdentifiers(t *testing.T) {
	got, err := CollectCurrentObjectFieldReferences(`PRIORVALUE(Amount) > 100 || ISCHANGED(Status__c) || Region__c == "APAC"`)
	if err != nil {
		t.Fatalf("CollectCurrentObjectFieldReferences() error = %v", err)
	}
	want := map[string]bool{"Amount": true, "Status__c": true, "Region__c": true}
	for _, name := range got {
		if !want[name] {
			t.Fatalf("CollectCurrentObjectFieldReferences() = %v: unexpected name %q (lowered/sentinel identifiers must resolve or be excluded)", got, name)
		}
		delete(want, name)
	}
	if len(want) != 0 {
		t.Fatalf("CollectCurrentObjectFieldReferences() = %v, missing %v", got, want)
	}
}

// Dynamic record member access reads the field named by the property's
// runtime VALUE — no static set covers it, so collection must fail with the
// sentinel instead of emitting the property expression's identifier as a
// bogus field (which would silently under-read any capped basis derived
// from the collection). Non-record dynamic access stays a plain non-match.
func TestCollectCurrentObjectFieldReferencesDynamicRecordAccessFails(t *testing.T) {
	for _, expr := range []string{
		`record[Name] == "x"`,
		`$Record[Name] == "x"`,
		`LEFT($Record[Name], 1) == "A"`,
		`prior[Selector__c] > 0`,
		// Nested forms: a computed property ANYWHERE in a record-rooted
		// chain is dynamic — memberChain drops computed segments, so
		// record[Selector].Amount would otherwise masquerade as a static
		// single-segment "Amount" chain (Codex round-4).
		`record[Selector].Amount > 0`,
		`$Record[Selector].Amount > 0`,
		`record["Static__c"][Selector] > 0`,
		`record[Outer][Inner] == "x"`,
		// Bare whole-record references read an unknowable field set — the
		// same unanalyzable class.
		`record != nil`,
		`len(prior) > 0`,
	} {
		if _, err := CollectCurrentObjectFieldReferences(expr); !errors.Is(err, ErrDynamicRecordFieldAccess) {
			t.Fatalf("CollectCurrentObjectFieldReferences(%q) error = %v, want ErrDynamicRecordFieldAccess", expr, err)
		}
	}
	// Static bracket access and non-record dynamic access are unaffected.
	got, err := CollectCurrentObjectFieldReferences(`record["Amount"] > 0 || Other[Name] == "x"`)
	if err != nil {
		t.Fatalf("CollectCurrentObjectFieldReferences() error = %v, want nil for static record access + non-record dynamic access", err)
	}
	if len(got) != 1 || got[0] != "Amount" {
		t.Fatalf("CollectCurrentObjectFieldReferences() = %v, want [Amount]", got)
	}
}

func TestCollectCurrentObjectFieldReferencesDeduplicates(t *testing.T) {
	got, err := CollectCurrentObjectFieldReferences(`Region__c == "APAC" || Region__c == "EMEA" || ISPICKVAL(Status__c, "Open")`)
	if err != nil {
		t.Fatalf("CollectCurrentObjectFieldReferences() error = %v", err)
	}
	want := []string{"Region__c", "Status__c"}
	if len(got) != len(want) {
		t.Fatalf("CollectCurrentObjectFieldReferences() = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("CollectCurrentObjectFieldReferences()[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
		}
	}
}
