package fieldprovenance

import (
	"sync"
	"testing"

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

// buildTestClassifier returns a Classifier seeded with representative
// fixture data across each L1 manifest. Mirrors the real-world layering
// without pulling in the embedded JSON files (kept as a pure-Go fixture
// so the package has no hidden dependencies).
func buildTestClassifier(t *testing.T) *Classifier {
	t.Helper()

	// L1 system tables — the bootstrap manifest. A field declared here is
	// platform-managed ("managed").
	systemTables := NewManifest()
	systemTables.Add("_System_Object", "Id", nil)
	systemTables.Add("_System_Field", "Source", nil)
	systemTables.Add("User", "Email", nil)
	systemTables.Add("User", "Name", &FieldDecl{
		IsCompound:   true,
		ComposedFrom: []string{"FirstName", "LastName"},
	})

	// L1 standard objects spec — standard CRM objects.
	standardSpec := NewManifest()
	standardSpec.Add("Account", "Name", nil)
	standardSpec.Add("Account", "Industry", nil)
	standardSpec.Add("Quote", "Name", &FieldDecl{
		IsCompound:   true,
		ComposedFrom: []string{"QuoteNumber"},
	})
	standardSpec.Add("Quote", "QuoteNumber", nil)

	// L1 tenant objects spec — tenant-specific overrides.
	tenantSpec := NewManifest()
	tenantSpec.Add("Account", "Product_Family__c", nil)
	tenantSpec.Add("Order", "PingCAP_RevRec_Entity__c", nil)

	// Platform standard-field template — fields injected into every custom
	// object by EnrichWithSystemFields.
	platformStd := []string{
		"Id", "Name", "OwnerId",
		"CreatedDate", "CreatedById",
		"LastModifiedDate", "LastModifiedById",
		"SystemModstamp", "IsDeleted",
	}

	return NewClassifier(systemTables, standardSpec, tenantSpec, platformStd)
}

func TestClassifySource_SystemTables(t *testing.T) {
	c := buildTestClassifier(t)

	cases := []struct {
		name   string
		obj    string
		field  string
		expect string
	}{
		{"system object system field", "_System_Object", "Id", constants.FieldSourceManaged},
		{"system table field, case insensitive obj", "_system_object", "Id", constants.FieldSourceManaged},
		{"system table field, case insensitive field", "_System_Object", "id", constants.FieldSourceManaged},
		{"User standard system field", "User", "Email", constants.FieldSourceManaged},
	}
	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)
			}
		})
	}
}

func TestClassifySource_StandardSpec(t *testing.T) {
	c := buildTestClassifier(t)

	if got := c.ClassifySource("Account", "Industry"); got != constants.FieldSourceStandardSpec {
		t.Fatalf("Account.Industry = %q; want %q", got, constants.FieldSourceStandardSpec)
	}
	if got := c.ClassifySource("Quote", "QuoteNumber"); got != constants.FieldSourceStandardSpec {
		t.Fatalf("Quote.QuoteNumber = %q; want %q", got, constants.FieldSourceStandardSpec)
	}
}

func TestClassifySource_TenantSpec(t *testing.T) {
	c := buildTestClassifier(t)

	// A __c-suffixed field declared in tenant spec MUST classify as
	// tenant_spec, not custom — manifest lookup beats suffix rule.
	if got := c.ClassifySource("Account", "Product_Family__c"); got != constants.FieldSourceTenantSpec {
		t.Fatalf("Account.Product_Family__c = %q; want %q (manifest must beat __c suffix)",
			got, constants.FieldSourceTenantSpec)
	}
	if got := c.ClassifySource("Order", "PingCAP_RevRec_Entity__c"); got != constants.FieldSourceTenantSpec {
		t.Fatalf("Order.PingCAP_RevRec_Entity__c = %q; want %q", got, constants.FieldSourceTenantSpec)
	}
}

func TestClassifySource_PlatformStandardField(t *testing.T) {
	c := buildTestClassifier(t)

	// Fields injected by EnrichWithSystemFields into any custom_object
	// must classify as managed. This is the exact path #1582 broke under
	// v1.5 (CreateSchema wrote these fields without a Source tag, and the
	// stored-Source model classified them as "discovered").
	customObj := "MyRegressionObj__c"
	for _, field := range []string{"Id", "Name", "OwnerId", "CreatedDate",
		"LastModifiedDate", "SystemModstamp", "IsDeleted"} {
		t.Run(field, func(t *testing.T) {
			got := c.ClassifySource(customObj, field)
			if got != constants.FieldSourceManaged {
				t.Fatalf("ClassifySource(%q, %q) = %q; want %q (platform standard-field template)",
					customObj, field, got, constants.FieldSourceManaged)
			}
		})
	}
}

func TestClassifySource_CustomSuffix(t *testing.T) {
	c := buildTestClassifier(t)

	// Admin-created custom fields with __c suffix on an object not declared
	// in tenant_spec — fall back to the suffix rule.
	if got := c.ClassifySource("MyObj__c", "Region__c"); got != constants.FieldSourceCustom {
		t.Fatalf("MyObj__c.Region__c = %q; want %q", got, constants.FieldSourceCustom)
	}
	if got := c.ClassifySource("Account", "Random_Custom_Field__c"); got != constants.FieldSourceCustom {
		t.Fatalf("Account.Random_Custom_Field__c (undeclared) = %q; want %q",
			got, constants.FieldSourceCustom)
	}
}

func TestClassifySource_Discovered(t *testing.T) {
	c := buildTestClassifier(t)

	// Physical column with no declaration and no __c suffix, but OBJECT is
	// declared in systemTables — the core #1582 case: legacy shadow
	// columns on User like UserPermissionsMarketingUser. Rule 7 fires.
	if got := c.ClassifySource("User", "UserPermissionsMarketingUser"); got != constants.FieldSourceDiscovered {
		t.Fatalf("legacy User.UserPermissionsMarketingUser = %q; want %q",
			got, constants.FieldSourceDiscovered)
	}
}

func TestClassifySource_UnknownObjectIsAdminCreated(t *testing.T) {
	c := buildTestClassifier(t)

	// Rule 5: object not in any L1 manifest → admin-created custom_object
	// (runtime CreateSchema). All its fields default to custom regardless
	// of naming convention. Replaces the old "__c suffix or discovered"
	// dichotomy for this case.
	if got := c.ClassifySource("UnknownObj", "Status"); got != constants.FieldSourceCustom {
		t.Fatalf("unknown object's non-__c field should be custom (rule 5); got %q",
			got)
	}
	if got := c.ClassifySource("MyObj__c", "AnyField"); got != constants.FieldSourceCustom {
		t.Fatalf("unknown object's non-__c field should be custom; got %q", got)
	}
}

func TestClassifySource_RulePriority(t *testing.T) {
	// Explicit tests that the documented precedence (systemTables >
	// standardSpec > tenantSpec > platformStd > suffix > discovered) is
	// actually enforced by the implementation.

	sys := NewManifest()
	sys.Add("Collision", "Name", nil)

	std := NewManifest()
	std.Add("Collision", "Name", nil)

	tnt := NewManifest()
	tnt.Add("Collision", "Name", nil)

	platformStd := []string{"Name"}

	// All three manifests + platformStd declare Collision.Name. The
	// systemTables layer wins.
	c := NewClassifier(sys, std, tnt, platformStd)
	if got := c.ClassifySource("Collision", "Name"); got != constants.FieldSourceManaged {
		t.Fatalf("priority: systemTables should win: got %q; want %q",
			got, constants.FieldSourceManaged)
	}

	// Remove systemTables → standardSpec wins.
	c2 := NewClassifier(nil, std, tnt, platformStd)
	if got := c2.ClassifySource("Collision", "Name"); got != constants.FieldSourceStandardSpec {
		t.Fatalf("priority: standardSpec next: got %q; want %q",
			got, constants.FieldSourceStandardSpec)
	}

	// Remove standardSpec → tenantSpec wins.
	c3 := NewClassifier(nil, nil, tnt, platformStd)
	if got := c3.ClassifySource("Collision", "Name"); got != constants.FieldSourceTenantSpec {
		t.Fatalf("priority: tenantSpec next: got %q; want %q",
			got, constants.FieldSourceTenantSpec)
	}

	// Remove tenantSpec → platformStd (managed) wins over rules 5/6/7.
	c4 := NewClassifier(nil, nil, nil, platformStd)
	if got := c4.ClassifySource("Collision", "Name"); got != constants.FieldSourceManaged {
		t.Fatalf("priority: platformStd next: got %q; want %q",
			got, constants.FieldSourceManaged)
	}

	// Rule 5 (object absent from all manifests) fires before rule 6 (__c
	// suffix) and rule 7 (discovered). With empty manifests, every
	// object is "unknown" — so every non-platform-standard field
	// classifies as custom via rule 5.
	c5 := NewClassifier(nil, nil, nil, nil)
	if got := c5.ClassifySource("UnknownObj", "Thing"); got != constants.FieldSourceCustom {
		t.Fatalf("priority: rule 5 (unknown object): got %q; want %q",
			got, constants.FieldSourceCustom)
	}

	// Rule 6 (__c suffix) only reachable when the object IS declared in
	// a manifest but the field isn't — simulates CreateField on a
	// standard_object.
	sysWithKnownObj := NewManifest()
	sysWithKnownObj.Add("Account", "Name", nil)
	c6 := NewClassifier(sysWithKnownObj, nil, nil, nil)
	if got := c6.ClassifySource("Account", "Foo__c"); got != constants.FieldSourceCustom {
		t.Fatalf("priority: rule 6 (__c on known object): got %q; want %q",
			got, constants.FieldSourceCustom)
	}

	// Rule 7 (default discovered): known object, unknown field, no __c —
	// the only remaining path for legacy discovered physical columns.
	if got := c6.ClassifySource("Account", "LegacyLeftover"); got != constants.FieldSourceDiscovered {
		t.Fatalf("priority: rule 7 (default discovered on known object): got %q; want %q",
			got, constants.FieldSourceDiscovered)
	}
}

func TestClassifySource_NilClassifier(t *testing.T) {
	// Defensive: a nil Classifier must not panic and must return a
	// reasonable fallback (discovered).
	var c *Classifier
	if got := c.ClassifySource("Account", "Name"); got != constants.FieldSourceDiscovered {
		t.Fatalf("nil classifier must return %q; got %q", constants.FieldSourceDiscovered, got)
	}
}

func TestClassifyCompound(t *testing.T) {
	c := buildTestClassifier(t)

	cases := []struct {
		name       string
		obj, field string
		expectOk   bool
		expectCF   []string
	}{
		{
			name:     "User.Name from systemTables",
			obj:      "User",
			field:    "Name",
			expectOk: true,
			expectCF: []string{"FirstName", "LastName"},
		},
		{
			name:     "Quote.Name from standardSpec",
			obj:      "Quote",
			field:    "Name",
			expectOk: true,
			expectCF: []string{"QuoteNumber"},
		},
		{
			name:     "non-compound field",
			obj:      "Account",
			field:    "Industry",
			expectOk: false,
			expectCF: nil,
		},
		{
			name:     "platform standard field on custom_object (not compound)",
			obj:      "MyObj__c",
			field:    "Name",
			expectOk: false,
			expectCF: nil,
		},
		{
			name:     "unknown object/field",
			obj:      "Unknown",
			field:    "Field",
			expectOk: false,
			expectCF: nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ok, cf := c.ClassifyCompound(tc.obj, tc.field)
			if ok != tc.expectOk {
				t.Fatalf("ClassifyCompound(%q,%q) ok = %v; want %v",
					tc.obj, tc.field, ok, tc.expectOk)
			}
			if len(cf) != len(tc.expectCF) {
				t.Fatalf("ClassifyCompound(%q,%q) cf = %v; want %v",
					tc.obj, tc.field, cf, tc.expectCF)
			}
			for i := range cf {
				if cf[i] != tc.expectCF[i] {
					t.Fatalf("ClassifyCompound cf[%d] = %q; want %q",
						i, cf[i], tc.expectCF[i])
				}
			}
		})
	}
}

func TestClassifyCompound_ReturnedSliceIsCopy(t *testing.T) {
	c := buildTestClassifier(t)

	_, cf := c.ClassifyCompound("User", "Name")
	if len(cf) == 0 {
		t.Fatal("expected non-empty ComposedFrom")
	}
	// Mutate the returned slice; subsequent lookup must not see the change.
	cf[0] = "Mutated"

	_, cfAgain := c.ClassifyCompound("User", "Name")
	if cfAgain[0] != "FirstName" {
		t.Fatalf("returned slice was not a defensive copy: got %q after mutation", cfAgain[0])
	}
}

func TestManifest_AddLookup_Basics(t *testing.T) {
	m := NewManifest()

	// Empty manifest: Lookup returns nil.
	if got := m.Lookup("Account", "Name"); got != nil {
		t.Fatalf("empty manifest Lookup should be nil; got %+v", got)
	}

	// Add → Lookup round-trip with mixed case.
	m.Add("Account", "Name", &FieldDecl{IsCompound: false})
	if got := m.Lookup("ACCOUNT", "name"); got == nil {
		t.Fatal("case-insensitive lookup failed")
	}

	// Add nil decl → placeholder, Lookup still returns non-nil.
	m.Add("Account", "OwnerId", nil)
	got := m.Lookup("Account", "OwnerId")
	if got == nil {
		t.Fatal("Add(nil) should still be lookup-able")
	}
	if got.ObjectAPIName != "Account" || got.FieldAPIName != "OwnerId" {
		t.Fatalf("nil-decl placeholder did not preserve APIName: %+v", got)
	}
}

func TestManifest_NilReceiver(t *testing.T) {
	var m *Manifest
	if got := m.Lookup("Anything", "Anything"); got != nil {
		t.Fatalf("nil manifest Lookup should be nil; got %+v", got)
	}
}

func TestClassifySource_EmptyInput(t *testing.T) {
	c := buildTestClassifier(t)

	// Empty object + empty field should fall through every rule to
	// FieldSourceDiscovered (no manifest has a "" key, "" has no __c
	// suffix, "" is not in the platform standard-field set). Defensive
	// test — no production code should call with empty strings, but we
	// want to guarantee no panic and a well-defined return.
	//
	// Empty object flows through the rules like any other: rule 5 fires
	// (an empty string is not a declared manifest key) and returns
	// custom. That's consistent with "unknown object = admin-created".
	if got := c.ClassifySource("", ""); got != constants.FieldSourceCustom {
		t.Fatalf("empty input with empty field still trips rule 5: got %q", got)
	}
	// Known object + empty field: rule 5 doesn't fire (Account is in
	// the test classifier's standardSpec), rule 4 has no platformStd
	// match for "", suffix check is false → default discovered.
	if got := c.ClassifySource("Account", ""); got != constants.FieldSourceDiscovered {
		t.Fatalf("empty field on known object should be %q; got %q",
			constants.FieldSourceDiscovered, got)
	}
	// Empty object + platform-standard field name: rule 4 fires (platformStd
	// is object-agnostic) and returns managed before rule 5 is even
	// reached. Documented property.
	if got := c.ClassifySource("", "Name"); got != constants.FieldSourceManaged {
		t.Fatalf("empty object with platform-std field should be %q; got %q",
			constants.FieldSourceManaged, got)
	}
}

func TestClassifier_ConcurrentRead(t *testing.T) {
	// Manifest.byObj is a map, which Go's memory model only guarantees
	// safe for concurrent reads when there are no concurrent writes. The
	// Classifier is constructed once in NewClassifier (Add is not called
	// after), so concurrent ClassifySource / ClassifyCompound from many
	// goroutines must be race-free. Run under `go test -race` to verify.
	c := buildTestClassifier(t)

	const goroutines = 64
	const iters = 200

	var wg sync.WaitGroup
	wg.Add(goroutines)
	for g := 0; g < goroutines; g++ {
		go func() {
			defer wg.Done()
			for i := 0; i < iters; i++ {
				if got := c.ClassifySource("User", "Email"); got != constants.FieldSourceManaged {
					t.Errorf("User.Email concurrent read got %q; want %q",
						got, constants.FieldSourceManaged)
					return
				}
				if ok, cf := c.ClassifyCompound("User", "Name"); !ok || len(cf) != 2 {
					t.Errorf("User.Name concurrent compound got (%v, %v); want (true, 2 members)",
						ok, cf)
					return
				}
				if got := c.ClassifySource("MyObj__c", "Random__c"); got != constants.FieldSourceCustom {
					t.Errorf("MyObj__c.Random__c concurrent read got %q; want %q",
						got, constants.FieldSourceCustom)
					return
				}
			}
		}()
	}
	wg.Wait()
}
