package fieldcapabilities

import (
	"testing"

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

// installTestClassifier seeds the fieldprovenance package-level default
// with the manifest shape the ComputeIntrinsic tests expect. Each test
// that relies on Layer 1d / 1e must install this first, otherwise the
// default nil classifier returns FieldSourceDiscovered for every call
// and every "managed" case fails.
//
// Restoration is registered via t.Cleanup so one failing test does not
// leak classifier state into another.
func installTestClassifier(t *testing.T) {
	t.Helper()

	prev := fieldprovenance.Default()
	t.Cleanup(func() { fieldprovenance.SetDefault(prev) })

	// systemTables-backed declarations — User infrastructure fields.
	sys := []fieldprovenance.ObjectDeclaration{
		{APIName: "User", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "FirstName"},
			{APIName: "LastName"},
			{APIName: "Name", IsCompound: true, ComposedFrom: []string{"FirstName", "LastName"}},
			{APIName: "FailedLoginAttempts"},
		}},
		{APIName: "Notification", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "notification_type"},
		}},
	}
	// standardSpec-backed declarations — standard CRM objects.
	std := []fieldprovenance.ObjectDeclaration{
		{APIName: "Account", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "Name"},
			{APIName: "Description"},
			{APIName: "Industry"},
		}},
		{APIName: "Campaign", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "NumberOfLeads"},
		}},
		{APIName: "Opportunity", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "Amount"},
		}},
		{APIName: "Order", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "Status"},
		}},
		{APIName: "Quote", Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "QuoteNumber"},
		}},
	}

	c := fieldprovenance.BuildClassifier(sys, std, nil, nil)
	fieldprovenance.SetDefault(c)
}

// TestComputeIntrinsic covers all ComputeIntrinsic Layer branches. Layer
// 1d (discovered) and Layer 1e (compound) are the two branches rewritten
// under issue #1582 RFC v1.6 to consult fieldprovenance.ClassifySource /
// ClassifyCompound instead of reading f.Source / f.IsCompound struct
// fields. The test cases below drive classification via the installed
// test classifier (installTestClassifier), not via struct fields.
func TestComputeIntrinsic(t *testing.T) {
	installTestClassifier(t)
	strPtr := func(s string) *string { return &s }

	tests := []struct {
		name       string
		objectName string
		field      *models.FieldMetadata
		wantCreate bool
		wantUpdate bool
	}{
		// Layer 1a — audit fields (independent of classifier)
		{
			name:       "1a: Id is never createable/updateable",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: constants.FieldID, Type: "string"},
		},
		{
			name:       "1a: CreatedDate blocks both",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: constants.FieldCreatedDate, Type: "dateTime"},
		},
		// Layer 1b — type-based
		{
			name:       "1b: formula field blocks both",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "ComputedValue", Type: "string", Formula: strPtr("FirstName + LastName")},
		},
		{
			name:       "1b: autoNumber blocks both",
			objectName: "Quote",
			field:      &models.FieldMetadata{APIName: "QuoteNumber", Type: constants.FieldTypeAutonumber},
		},
		{
			name:       "1b: rollupSummary blocks both",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "TotalOps", Type: constants.FieldTypeRollupSummary},
		},
		{
			name:       "1b: suffix alone does not imply polymorphic discriminator",
			objectName: "Notification",
			field:      &models.FieldMetadata{APIName: "notification_type", Type: constants.FieldTypeString},
			wantCreate: true,
			wantUpdate: true,
		},
		{
			name:       "1b: write protection policy blocks both",
			objectName: "ProcessInstanceWorkitem",
			field: &models.FieldMetadata{
				APIName:               "ActorId",
				Type:                  constants.FieldTypeReference,
				WriteProtectionPolicy: constants.FieldWriteProtectionTrustedOnly,
			},
		},
		{
			name:       "1b: catalog authority policy blocks both",
			objectName: "_System_Field",
			field: &models.FieldMetadata{
				APIName:               "WriteProtectionPolicy",
				Type:                  constants.FieldTypeString,
				WriteProtectionPolicy: constants.FieldWriteProtectionAuthorityOnly,
			},
		},
		{
			name:       "1b: immutable-after-create remains createable but blocks update",
			objectName: "Account",
			field: &models.FieldMetadata{
				APIName:               "InitialValue__c",
				Type:                  constants.FieldTypeString,
				WriteProtectionPolicy: constants.FieldWriteProtectionImmutableAfterCreate,
			},
			wantCreate: true,
			wantUpdate: false,
		},
		// Layer 1c — system dates (independent of classifier)
		{
			name:       "1c: LastActivityDate blocks both",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "LastActivityDate", Type: "date"},
		},
		// Layer 1d — classifier-backed discovery. Under v1.6 Source is
		// derived from L1 manifests; these cases exercise the ClassifySource
		// rules by picking (object, field) tuples that the test classifier
		// does or does not declare.
		{
			// Not declared anywhere + no __c suffix + not in platformStd
			// → classifier returns FieldSourceDiscovered → Layer 1d blocks.
			name:       "1d: undeclared column blocks both (discovered)",
			objectName: "User",
			field:      &models.FieldMetadata{APIName: "UserPermissionsMarketingUser", Type: "boolean"},
		},
		{
			// Declared in systemTables → managed → Layer 1d does NOT block.
			name:       "1d: systemTables-declared field is createable",
			objectName: "User",
			field:      &models.FieldMetadata{APIName: "FirstName", Type: "string"},
			wantCreate: true,
			wantUpdate: true,
		},
		{
			// Declared in standardSpec → standard_spec → Layer 1d does NOT
			// block. Regression guard: empty f.Source on the struct must not
			// silently turn into "discovered" in the classifier's eyes (v1.5
			// misbehavior — we now route entirely through the classifier).
			name:       "1d: standardSpec-declared field is createable regardless of struct Source",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "Description", Type: "textarea"},
			wantCreate: true,
			wantUpdate: true,
		},
		// Layer 1e — classifier-backed compound detection.
		{
			// systemTables declares User.Name as compound → Layer 1e blocks.
			name:       "1e: compound parent (User.Name from classifier) blocks both",
			objectName: "User",
			field:      &models.FieldMetadata{APIName: "Name", Type: "string"},
		},
		{
			// standardSpec declares Account.Name but NOT compound → Layer
			// 1e does NOT block.
			name:       "1e: non-compound Account.Name is createable",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "Name", Type: "string"},
			wantCreate: true,
			wantUpdate: true,
		},
		// Application-pack facts are not encoded here. CRM fields that pass
		// universal intrinsic checks are writable unless their pack metadata
		// declares otherwise in _System_Field.
		{
			name:       "app-pack field: Campaign.NumberOfLeads is not blocked in L0",
			objectName: "Campaign",
			field:      &models.FieldMetadata{APIName: "NumberOfLeads", Type: "int"},
			wantCreate: true,
			wantUpdate: true,
		},
		{
			name:       "platform field: User.FailedLoginAttempts blocked by denylist",
			objectName: "User",
			field:      &models.FieldMetadata{APIName: "FailedLoginAttempts", Type: "int"},
		},
		// Default — declared field, not compound, not denylisted.
		{
			name:       "default: standardSpec field is createable+updateable",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "Industry", Type: "picklist"},
			wantCreate: true,
			wantUpdate: true,
		},
		{
			// No manifest declaration but __c suffix → classifier rule 5 →
			// FieldSourceCustom → Layer 1d does NOT block.
			name:       "default: __c suffix rule produces FieldSourceCustom",
			objectName: "Account",
			field:      &models.FieldMetadata{APIName: "My_Custom__c", Type: "string"},
			wantCreate: true,
			wantUpdate: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			gotCreate, gotUpdate := ComputeIntrinsic(tc.objectName, tc.field)
			if gotCreate != tc.wantCreate {
				t.Errorf("createable = %v, want %v", gotCreate, tc.wantCreate)
			}
			if gotUpdate != tc.wantUpdate {
				t.Errorf("updateable = %v, want %v", gotUpdate, tc.wantUpdate)
			}
		})
	}
}

func TestApplyIntrinsicBounds(t *testing.T) {
	installTestClassifier(t)

	declaredReadOnly := &models.FieldMetadata{APIName: "Industry", Type: "picklist"}
	c, u := ApplyIntrinsicBounds("Account", declaredReadOnly, false, false)
	if c || u {
		t.Fatalf("declared read-only field must remain false/false, got c=%v u=%v", c, u)
	}

	formula := "1"
	declaredWritableFormula := &models.FieldMetadata{APIName: "ComputedValue", Type: "string", Formula: &formula}
	c, u = ApplyIntrinsicBounds("Account", declaredWritableFormula, true, true)
	if c || u {
		t.Fatalf("universal intrinsic bounds must still force formula read-only, got c=%v u=%v", c, u)
	}
}

func TestComputeIntrinsic_ImmutableAfterCreateDoesNotOverrideUniversalReadOnly(t *testing.T) {
	installTestClassifier(t)

	tests := []struct {
		name       string
		objectName string
		field      *models.FieldMetadata
	}{
		{
			name:       "system-maintained date",
			objectName: "Account",
			field: &models.FieldMetadata{
				APIName:               constants.FieldLastActivityDate,
				Type:                  "date",
				WriteProtectionPolicy: constants.FieldWriteProtectionImmutableAfterCreate,
			},
		},
		{
			name:       "discovered field",
			objectName: "User",
			field: &models.FieldMetadata{
				APIName:               "UserPermissionsMarketingUser",
				Type:                  "boolean",
				WriteProtectionPolicy: constants.FieldWriteProtectionImmutableAfterCreate,
			},
		},
		{
			name:       "compound field",
			objectName: "User",
			field: &models.FieldMetadata{
				APIName:               "Name",
				Type:                  "string",
				WriteProtectionPolicy: constants.FieldWriteProtectionImmutableAfterCreate,
			},
		},
		{
			name:       "platform denylisted field",
			objectName: "User",
			field: &models.FieldMetadata{
				APIName:               "FailedLoginAttempts",
				Type:                  "int",
				WriteProtectionPolicy: constants.FieldWriteProtectionImmutableAfterCreate,
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			createable, updateable := ComputeIntrinsic(tc.objectName, tc.field)
			if createable || updateable {
				t.Fatalf("immutable-after-create must not override universal read-only, got createable=%v updateable=%v", createable, updateable)
			}
		})
	}
}

func TestComputeIntrinsicDoesNotEmbedCRMObjectDenylist(t *testing.T) {
	installTestClassifier(t)

	tests := []struct {
		objectName string
		field      *models.FieldMetadata
	}{
		{
			objectName: "Campaign",
			field:      &models.FieldMetadata{APIName: "NumberOfLeads", Type: "int"},
		},
		{
			objectName: "Order",
			field:      &models.FieldMetadata{APIName: "Status", Type: "picklist"},
		},
		{
			objectName: "Opportunity",
			field:      &models.FieldMetadata{APIName: "Amount", Type: "currency"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.objectName+"."+tc.field.APIName, func(t *testing.T) {
			c, u := ComputeIntrinsic(tc.objectName, tc.field)
			if !c || !u {
				t.Fatalf("CRM-shaped field was blocked by L0 intrinsic rules: createable=%v updateable=%v", c, u)
			}
		})
	}
}

// TestComputeIntrinsic_Layer1dPrecedence verifies an undeclared field
// (classified as FieldSourceDiscovered at runtime) remains non-writable
// even when it carries a __c-like name that the default rule would
// otherwise accept. With no manifest declaration and no __c suffix, the
// classifier returns discovered; Layer 1d wins over Layer 2 and the
// default branch.
func TestComputeIntrinsic_Layer1dPrecedence(t *testing.T) {
	installTestClassifier(t)

	f := &models.FieldMetadata{APIName: "CustomLegacyCol", Type: "string"}
	c, u := ComputeIntrinsic("Account", f)
	if c || u {
		t.Errorf("discovered field must not be createable/updateable, got c=%v u=%v", c, u)
	}
}

// TestComputeIntrinsic_Layer1ePrecedence verifies a compound parent is
// non-writable even when the object has no Layer 2 denylist entry. The
// test classifier declares User.Name as compound via systemTables.
func TestComputeIntrinsic_Layer1ePrecedence(t *testing.T) {
	installTestClassifier(t)

	f := &models.FieldMetadata{APIName: "Name", Type: "string"}
	c, u := ComputeIntrinsic("User", f)
	if c || u {
		t.Errorf("compound parent must not be createable/updateable, got c=%v u=%v", c, u)
	}
}

// TestComputeIntrinsic_NoClassifierDefault_SafeFallback verifies the
// absence of an installed default classifier does not panic — it falls
// through to FieldSourceDiscovered, which means Layer 1d blocks. This
// matches the safety posture in fieldprovenance.ClassifySource and
// preserves the conservative "deny-by-default" intrinsic capability
// during pre-init startup ordering.
func TestComputeIntrinsic_NoClassifierDefault_SafeFallback(t *testing.T) {
	prev := fieldprovenance.Default()
	fieldprovenance.SetDefault(nil)
	t.Cleanup(func() { fieldprovenance.SetDefault(prev) })

	f := &models.FieldMetadata{APIName: "AnyField", Type: "string"}
	c, u := ComputeIntrinsic("Account", f)
	if c || u {
		t.Errorf("with nil classifier default, all fields classify as discovered and Layer 1d must block; got c=%v u=%v", c, u)
	}
}
