package fieldprovenance

import (
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
)

func TestBuildClassifier_FromDeclarations(t *testing.T) {
	sys := []ObjectDeclaration{
		{APIName: "User", Fields: []FieldDeclaration{
			{APIName: "Email"},
			{APIName: "Name", IsCompound: true, ComposedFrom: []string{"FirstName", "LastName"}},
		}},
	}
	std := []ObjectDeclaration{
		{APIName: "Account", Fields: []FieldDeclaration{
			{APIName: "Industry"},
			{APIName: "Name"},
		}},
	}
	tnt := []ObjectDeclaration{
		{APIName: "Account", Fields: []FieldDeclaration{
			{APIName: "Product_Family__c"},
		}},
	}
	platformStd := []string{"Id", "Name", "OwnerId", "CreatedDate"}

	c := BuildClassifier(sys, std, tnt, platformStd)

	cases := []struct {
		name   string
		obj    string
		field  string
		expect string
	}{
		{"systemTables wins", "User", "Email", constants.FieldSourceManaged},
		{"standardSpec wins", "Account", "Industry", constants.FieldSourceStandardSpec},
		{"tenantSpec on __c", "Account", "Product_Family__c", constants.FieldSourceTenantSpec},
		{"platformStd on custom_object", "MyObj__c", "Id", constants.FieldSourceManaged},
		{"rule 5 (unknown object) beats __c suffix", "MyObj__c", "Region__c", constants.FieldSourceCustom},
		{"rule 5 (unknown object, no __c)", "BrandNewAdminObj", "Status", constants.FieldSourceCustom},
		// Legacy discovered: object IS declared, but this specific field isn't
		// and has no __c suffix → rule 7 fires.
		{"rule 7 (discovered) on declared object", "User", "LegacyShadowCol", constants.FieldSourceDiscovered},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := c.ClassifySource(tc.obj, tc.field)
			if got != tc.expect {
				t.Fatalf("ClassifySource(%q,%q) = %q; want %q",
					tc.obj, tc.field, got, tc.expect)
			}
		})
	}

	// Compound propagated through BuildClassifier correctly.
	ok, cf := c.ClassifyCompound("User", "Name")
	if !ok || len(cf) != 2 || cf[0] != "FirstName" || cf[1] != "LastName" {
		t.Fatalf("ClassifyCompound(User, Name) = (%v, %v); want (true, [FirstName LastName])", ok, cf)
	}
}

func TestBuildClassifier_NilInputs(t *testing.T) {
	// All-nil inputs → empty manifests, default-platform, no acks.
	c := BuildClassifier(nil, nil, nil, nil)
	if c == nil {
		t.Fatal("BuildClassifier with all-nil inputs returned nil")
	}
	// With no manifests declared at all, every object looks unknown →
	// rule 5 (object absent from every manifest) fires → custom. That's
	// the correct new-semantic answer: a truly empty classifier can't
	// distinguish legacy from admin-created, so the charitable call is
	// admin-created (writable).
	if got := c.ClassifySource("Anything", "Anything"); got != constants.FieldSourceCustom {
		t.Fatalf("empty classifier should classify unknown-object as custom (rule 5); got %q", got)
	}
	if got := c.ClassifySource("Anything", "Foo__c"); got != constants.FieldSourceCustom {
		t.Fatalf("__c suffix or rule 5 should produce custom; got %q", got)
	}
}

func TestBuildClassifier_SkipsEmptyAPINames(t *testing.T) {
	// Defensive: decl entries with empty APIName must be skipped (they
	// would corrupt the manifest index if added).
	sys := []ObjectDeclaration{
		{APIName: "", Fields: []FieldDeclaration{{APIName: "Foo"}}},           // skipped
		{APIName: "User", Fields: []FieldDeclaration{{APIName: ""}}},          // skipped
		{APIName: "User", Fields: []FieldDeclaration{{APIName: "RealField"}}}, // kept
	}
	c := BuildClassifier(sys, nil, nil, nil)

	if got := c.ClassifySource("User", "RealField"); got != constants.FieldSourceManaged {
		t.Fatalf("valid decl should classify as managed; got %q", got)
	}
	// The empty-object decl never got added to the manifest, so an
	// empty-object query hits rule 5 (unknown object → custom). The
	// assertion here is really about "empty-name decls didn't get in" —
	// we check that the real User.RealField is still managed (above),
	// and that a KNOWN object with the empty field name yields
	// discovered (below), confirming the empty decl didn't register.
	if got := c.ClassifySource("User", ""); got != constants.FieldSourceDiscovered {
		t.Fatalf("empty-field decl must not register; got %q", got)
	}
}

func TestBuildClassifier_ComposedFromIsCopied(t *testing.T) {
	// Defensive copy on input: mutating the caller's slice after
	// BuildClassifier must not change classifier behavior.
	members := []string{"FirstName", "LastName"}
	sys := []ObjectDeclaration{
		{APIName: "User", Fields: []FieldDeclaration{
			{APIName: "Name", IsCompound: true, ComposedFrom: members},
		}},
	}
	c := BuildClassifier(sys, nil, nil, nil)

	// Mutate original after build.
	members[0] = "Tampered"

	_, cf := c.ClassifyCompound("User", "Name")
	if len(cf) == 0 || cf[0] != "FirstName" {
		t.Fatalf("input mutation leaked into classifier: got %v", cf)
	}
}

func TestDefaultClassifier_SetAndGet(t *testing.T) {
	// Save + restore the global at end so other tests are unaffected.
	prev := Default()
	t.Cleanup(func() { SetDefault(prev) })

	// Nil default — package-level funcs fall back safely.
	SetDefault(nil)
	if got := ClassifySource("Account", "Name"); got != constants.FieldSourceDiscovered {
		t.Fatalf("nil default should return discovered; got %q", got)
	}
	if ok, cf := ClassifyCompound("Account", "Name"); ok || cf != nil {
		t.Fatalf("nil default ClassifyCompound = (%v, %v); want (false, nil)", ok, cf)
	}

	// Install a concrete classifier and verify delegation.
	sys := []ObjectDeclaration{
		{APIName: "User", Fields: []FieldDeclaration{
			{APIName: "Name", IsCompound: true, ComposedFrom: []string{"FirstName", "LastName"}},
		}},
	}
	c := BuildClassifier(sys, nil, nil, []string{"Id"})
	SetDefault(c)

	if got := ClassifySource("User", "Name"); got != constants.FieldSourceManaged {
		t.Fatalf("package-level ClassifySource delegation broken: got %q", got)
	}
	ok, cf := ClassifyCompound("User", "Name")
	if !ok || len(cf) != 2 {
		t.Fatalf("package-level ClassifyCompound delegation broken: got (%v, %v)", ok, cf)
	}
}

func TestDefaultClassifier_ConcurrentSetAndClassify(t *testing.T) {
	// atomic.Pointer guarantees a consistent view even under concurrent
	// SetDefault + ClassifySource. This exercises the race-detector.
	prev := Default()
	t.Cleanup(func() { SetDefault(prev) })

	c1 := BuildClassifier(nil, nil, nil, []string{"Name"})
	c2 := BuildClassifier(nil, nil, nil, nil)

	done := make(chan struct{})
	go func() {
		defer close(done)
		for i := 0; i < 500; i++ {
			if i%2 == 0 {
				SetDefault(c1)
			} else {
				SetDefault(c2)
			}
		}
	}()

	for i := 0; i < 500; i++ {
		// Must not panic, must not race. Result can be either c1 or c2
		// view of the world — we only care about consistency.
		_ = ClassifySource("Anything", "Name")
	}
	<-done
}
