package formula

import (
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/formula/spec"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// widgetSchema returns a minimal ObjectMetadata with common fields used by lock
// rule validation tests. Includes Status (string), IsArchived (boolean), and
// Amount (double) so expression type-checking can resolve these identifiers.
func widgetSchema() *models.ObjectMetadata {
	return &models.ObjectMetadata{
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "Status", Type: constants.FieldTypeString},
			{APIName: "IsArchived", Type: constants.FieldTypeBoolean},
			{APIName: "Amount", Type: constants.FieldTypeDouble},
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}
}

// widgetSchemaWithRelationship returns a schema that includes a reference field
// with a RelationshipName so tests can verify that ordinary relationship traversal
// (e.g. Parent.Name) is rejected by the LockRule validator even though the
// schema has enough metadata to resolve it.
func widgetSchemaWithRelationship() *models.ObjectMetadata {
	parentRel := "Parent"
	return &models.ObjectMetadata{
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "Status", Type: constants.FieldTypeString},
			{APIName: "IsArchived", Type: constants.FieldTypeBoolean},
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				APIName:          "ParentId",
				Type:             constants.FieldTypeReference,
				RelationshipName: &parentRel,
				ReferenceTo:      []string{"Widget__c"},
			},
		},
	}
}

// TestValidateLockRuleExpression covers the schema-aware validation strategy
// for LockRule expressions. See schema_validation.go ValidateLockRuleExpression.
func TestValidateLockRuleExpression(t *testing.T) {
	eng := NewEngine()
	schema := widgetSchema()

	tests := []struct {
		name    string
		expr    string
		wantErr bool
	}{
		// Empty / blank — always valid (no LockRule)
		{name: "empty string", expr: "", wantErr: false},
		{name: "whitespace only", expr: "   ", wantErr: false},

		// Boolean-returning expressions with known record fields — VALID
		{
			name:    "equality comparison on known string field",
			expr:    `Status == 'Closed'`,
			wantErr: false,
		},
		{
			name:    "boolean field reference",
			expr:    `IsArchived == true`,
			wantErr: false,
		},
		{
			name:    "ISBLANK on known field",
			expr:    `ISBLANK(Status)`,
			wantErr: false,
		},

		// Session global comparisons — VALID ($User, $Profile, $UserRole are supported)
		{
			name:    "session global profile comparison",
			expr:    `$Profile.Name == 'Sales'`,
			wantErr: false,
		},
		{
			name:    "session global user role comparison",
			expr:    `$UserRole.Name == 'Manager'`,
			wantErr: false,
		},
		{
			name:    "session global user id comparison",
			expr:    `$User.Id == 'some-id'`,
			wantErr: false,
		},
		{
			name:    "combined record field and session global",
			expr:    `Status == 'Closed' && $Profile.Name == 'Sales'`,
			wantErr: false,
		},

		// Non-boolean result — INVALID
		{
			name:    "bare field reference is not boolean",
			expr:    `Status`,
			wantErr: true,
		},

		// Syntax errors — INVALID
		{
			name:    "unmatched paren",
			expr:    `Status == 'Closed' )`,
			wantErr: true,
		},

		// Unknown function — INVALID (ValidateSyntax rejects unregistered function names)
		{
			name:    "unknown function VLOOKUP is rejected",
			expr:    `VLOOKUP(Status, 'Closed')`,
			wantErr: true,
		},

		// Unknown field reference — INVALID (field not in object schema)
		{
			name:    "typo'd field MissingField is rejected",
			expr:    `MissingField == 'x'`,
			wantErr: true,
		},

		// $Permission — INVALID (Permission context is never injected in lock gate)
		{
			name:    "$Permission.ExportReports is rejected",
			expr:    `$Permission.ExportReports == true`,
			wantErr: true,
		},

		// Relationship traversal — INVALID (lock gate has no Fetcher)
		{
			name:    "Parent__r.Status cross-object path is rejected",
			expr:    `Parent__r.Status == 'Closed'`,
			wantErr: true,
		},

		// Unsupported session global root — INVALID
		{
			name:    "$Setup.* unsupported global is rejected",
			expr:    `$Setup.SomeSetting__c == 'value'`,
			wantErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidateLockRuleExpression(tc.expr, eng, schema)
			if tc.wantErr && err == nil {
				t.Fatalf("ValidateLockRuleExpression(%q) = nil; want error", tc.expr)
			}
			if !tc.wantErr && err != nil {
				t.Fatalf("ValidateLockRuleExpression(%q) = %v; want nil", tc.expr, err)
			}
		})
	}
}

// TestValidateLockRuleOrdinaryRelationshipRejected verifies that ordinary
// relationship traversal (e.g. Parent.Name) is rejected even when the object
// schema has a reference field with a RelationshipName that would normally let
// the schema walker resolve the path. The LockRule runtime gate has no Fetcher,
// so such paths would always fail at runtime; we block them at save time.
func TestValidateLockRuleOrdinaryRelationshipRejected(t *testing.T) {
	eng := NewEngine()
	schema := widgetSchemaWithRelationship()

	tests := []struct {
		name    string
		expr    string
		wantErr bool
	}{
		{
			name:    "ordinary relationship traversal Parent.Name is rejected",
			expr:    `Parent.Name == 'root'`,
			wantErr: true,
		},
		{
			name:    "ordinary relationship traversal Parent.Status is rejected",
			expr:    `Parent.Status == 'Active'`,
			wantErr: true,
		},
		{
			name:    "direct FK field OwnerId access is still valid",
			expr:    `ParentId == 'some-id'`,
			wantErr: false,
		},
		{
			name:    "scalar field comparison still works",
			expr:    `Status == 'Closed'`,
			wantErr: false,
		},
		{
			name:    "custom __r relationship traversal still rejected by AST walker",
			expr:    `Parent__r.Status == 'Closed'`,
			wantErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidateLockRuleExpression(tc.expr, eng, schema)
			if tc.wantErr && err == nil {
				t.Fatalf("ValidateLockRuleExpression(%q) = nil; want error", tc.expr)
			}
			if !tc.wantErr && err != nil {
				t.Fatalf("ValidateLockRuleExpression(%q) = %v; want nil", tc.expr, err)
			}
		})
	}
}

// TestValidateLockRuleExpressionNilEngine pins the nil-engine guard.
func TestValidateLockRuleExpressionNilEngine(t *testing.T) {
	if err := ValidateLockRuleExpression("Status == 'Closed'", nil, widgetSchema()); err == nil {
		t.Fatal("expected error for nil engine, got nil")
	}
}

// TestValidateLockRuleExpressionNilSchema pins the nil-schema guard.
func TestValidateLockRuleExpressionNilSchema(t *testing.T) {
	if err := ValidateLockRuleExpression("Status == 'Closed'", NewEngine(), nil); err == nil {
		t.Fatal("expected error for nil schema, got nil")
	}
}

// TestValidateLockRuleExpression_RejectsInternalLoweringArtifact verifies that
// authored LockRule expressions referencing internal lowering tokens are
// rejected even though __NCRM_FIELDUNCHANGED__ is a registered catalog
// function and would otherwise pass the unknown-function check.
func TestValidateLockRuleExpression_RejectsInternalLoweringArtifact(t *testing.T) {
	eng := NewEngine()
	schema := widgetSchema()

	expr := `__NCRM_FIELDUNCHANGED__("Status", null, null) == true`
	if err := ValidateLockRuleExpression(expr, eng, schema); err == nil {
		t.Fatalf("ValidateLockRuleExpression(%q) = nil; want error rejecting internal lowering artifact", expr)
	}
}

func TestValidateLockRuleExpressionUsesInjectedFunctionRegistry(t *testing.T) {
	registry := spec.DefaultRegistry().Clone()
	if err := registry.Register(spec.FunctionSpec{
		Name:    "IS_POSITIVE",
		MinArgs: 1,
		MaxArgs: 1,
		Return:  spec.ReturnSpec{Kind: spec.ReturnFixed, Fixed: spec.TypeBoolean},
		Args:    []spec.ArgSpec{{Type: spec.TypeNumber}},
	}); err != nil {
		t.Fatalf("register IS_POSITIVE spec: %v", err)
	}

	eng := NewEngine(WithFunctionRegistry(registry))
	schema := widgetSchema()

	if err := ValidateLockRuleExpression(`is_positive(Amount)`, eng, schema); err != nil {
		t.Fatalf("ValidateLockRuleExpression lower-case injected function = %v; want nil", err)
	}
	err := ValidateLockRuleExpression(`IS_POSITIVE(Name)`, eng, schema)
	if err == nil || !strings.Contains(err.Error(), "IS_POSITIVE argument 1 requires number") {
		t.Fatalf("ValidateLockRuleExpression injected function over string = %v; want schema-aware argument-type rejection", err)
	}
}
