package metadata

import (
	"context"
	"strings"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	domainschema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// TestUpdateObjectMarksLockRuleAsCustomizedAndSurvivesPackReapply verifies
// that an admin PATCH to LockRule on a defaults_only object:
//  1. sets LockRuleCustomized=true on the saved object, and
//  2. the updated value is preserved (i.e. the saved object holds the new rule).
//
// This is the regression test for the gate-review finding: UpdateSchema was
// flipping IsCustomized but not LockRuleCustomized, causing pack verify/reapply
// to treat the admin-authored rule as pack-owned drift.
func TestUpdateObjectMarksLockRuleAsCustomizedAndSurvivesPackReapply(t *testing.T) {
	ctx := context.Background()

	packLockRule := "Status == 'Open'"
	adminLockRule := "IsArchived"

	// Object as pack-installed: defaults_only, LockRule set by pack, not customized.
	initial := &models.ObjectMetadata{
		ID:                 "obj_widget",
		APIName:            "Widget__c",
		LockRule:           &packLockRule,
		LockRuleCustomized: false,
		Fields: []models.FieldMetadata{
			{APIName: "Status", Type: constants.FieldTypeString},
			{APIName: "IsArchived", Type: constants.FieldTypeBoolean},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	// Admin PATCH: change LockRule to a different synthetic expression.
	updates := &models.ObjectMetadata{
		LockRule: &adminLockRule,
	}
	if err := svc.UpdateSchema(ctx, "Widget__c", updates); err != nil {
		t.Fatalf("UpdateSchema: %v", err)
	}

	saved := repo.stored
	if saved.LockRule == nil || *saved.LockRule != adminLockRule {
		t.Errorf("LockRule = %v, want %q", saved.LockRule, adminLockRule)
	}
	if !saved.LockRuleCustomized {
		t.Error("LockRuleCustomized = false, want true after admin LockRule edit")
	}
}

// TestUpdateObjectDoesNotFlipLockRuleCustomizedWhenValueUnchanged verifies that
// UpdateSchema does NOT set LockRuleCustomized when the incoming LockRule value
// is identical to the current one. This mirrors the ListFieldsCustomized trigger
// logic (only flipped on actual value change).
func TestUpdateObjectDoesNotFlipLockRuleCustomizedWhenValueUnchanged(t *testing.T) {
	ctx := context.Background()

	rule := "Status == 'Open'"
	initial := &models.ObjectMetadata{
		ID:                 "obj_widget",
		APIName:            "Widget__c",
		LockRule:           &rule,
		LockRuleCustomized: false,
		Fields: []models.FieldMetadata{
			{APIName: "Status", Type: constants.FieldTypeString},
			{APIName: "IsArchived", Type: constants.FieldTypeBoolean},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	// PATCH with the same value — no change.
	sameLockRule := "Status == 'Open'"
	updates := &models.ObjectMetadata{
		LockRule: &sameLockRule,
	}
	if err := svc.UpdateSchema(ctx, "Widget__c", updates); err != nil {
		t.Fatalf("UpdateSchema: %v", err)
	}

	if repo.stored.LockRuleCustomized {
		t.Error("LockRuleCustomized = true, want false when value is unchanged")
	}
}

func TestUpdateSchemaRejectsInvalidLockRuleExpression(t *testing.T) {
	ctx := context.Background()

	initialRule := "IsArchived == true"
	invalidRule := "Status"
	initial := &models.ObjectMetadata{
		ID:                 "obj_widget",
		APIName:            "Widget__c",
		LockRule:           &initialRule,
		LockRuleCustomized: false,
		Fields: []models.FieldMetadata{
			{APIName: "Status", Type: constants.FieldTypeString},
			{APIName: "IsArchived", Type: constants.FieldTypeBoolean},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{LockRule: &invalidRule})
	if err == nil {
		t.Fatal("UpdateSchema(invalid lock rule) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(invalid lock rule) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "boolean") {
		t.Fatalf("UpdateSchema(invalid lock rule) err = %q, want boolean-type validation detail", err.Error())
	}
	if repo.stored.LockRule == nil || *repo.stored.LockRule != initialRule {
		t.Fatalf("stored LockRule = %v, want unchanged %q after validation failure", repo.stored.LockRule, initialRule)
	}
}

func TestCreateSchemaRejectsReadSourceAuthoring(t *testing.T) {
	svc := NewMetadataService(nil, nil, nil)
	err := svc.CreateSchema(context.Background(), &models.ObjectMetadata{
		APIName: "Widget__c",
		Label:   "Widget",
		ReadSource: &models.ObjectReadSourceConfig{
			Kind:          "physical_table",
			PhysicalTable: "_System_RecordType",
		},
	})
	if err == nil {
		t.Fatal("CreateSchema(readSource) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("CreateSchema(readSource) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), constants.FieldSysObject_ReadSource) {
		t.Fatalf("CreateSchema(readSource) err = %q, want field name %q", err.Error(), constants.FieldSysObject_ReadSource)
	}
}

func TestCreateSchemaOptimizedRejectsReadSourceAuthoring(t *testing.T) {
	svc := NewMetadataService(nil, nil, nil)
	err := svc.CreateSchemaOptimized(context.Background(), &models.ObjectMetadata{
		APIName: "Widget__c",
		Label:   "Widget",
		ReadSource: &models.ObjectReadSourceConfig{
			Kind:          "physical_table",
			PhysicalTable: "_System_RecordType",
		},
	})
	if err == nil {
		t.Fatal("CreateSchemaOptimized(readSource) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("CreateSchemaOptimized(readSource) err = %T, want *ValidationError", err)
	}
}

func TestCreateSchemaRejectsExclusiveSiblingFieldsAuthoring(t *testing.T) {
	svc := NewMetadataService(nil, nil, nil)
	err := svc.CreateSchema(context.Background(), &models.ObjectMetadata{
		APIName: "Widget__c",
		Label:   "Widget",
		ExclusiveSiblingFields: []models.ExclusiveSiblingField{{
			FieldAPIName:          "IsDefault__c",
			PartitionFieldAPIName: "Parent__c",
		}},
	})
	if err == nil {
		t.Fatal("CreateSchema(exclusiveSiblingFields) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("CreateSchema(exclusiveSiblingFields) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), constants.FieldSysObject_ExclusiveSiblingFields) {
		t.Fatalf("CreateSchema(exclusiveSiblingFields) err = %q, want field name %q", err.Error(), constants.FieldSysObject_ExclusiveSiblingFields)
	}
}

func TestCreateSchemaOptimizedRejectsExclusiveSiblingFieldsAuthoring(t *testing.T) {
	svc := NewMetadataService(nil, nil, nil)
	err := svc.CreateSchemaOptimized(context.Background(), &models.ObjectMetadata{
		APIName:                          "Widget__c",
		Label:                            "Widget",
		ExclusiveSiblingFields:           []models.ExclusiveSiblingField{},
		ExclusiveSiblingFieldsCustomized: true,
	})
	if err == nil {
		t.Fatal("CreateSchemaOptimized(exclusiveSiblingFields) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("CreateSchemaOptimized(exclusiveSiblingFields) err = %T, want *ValidationError", err)
	}
}

func TestCreateSchemaRejectsRelationshipDotPathDisplayNameFieldWithBadChild(t *testing.T) {
	assertCreateSchemaRejectsBadRelationshipDisplayName(t, func(svc *MetadataService, schema *models.ObjectMetadata) error {
		return svc.CreateSchema(context.Background(), schema)
	})
}

func TestCreateSchemaOptimizedRejectsRelationshipDotPathDisplayNameFieldWithBadChild(t *testing.T) {
	assertCreateSchemaRejectsBadRelationshipDisplayName(t, func(svc *MetadataService, schema *models.ObjectMetadata) error {
		return svc.CreateSchemaOptimized(context.Background(), schema)
	})
}

func assertCreateSchemaRejectsBadRelationshipDisplayName(t *testing.T, create func(*MetadataService, *models.ObjectMetadata) error) {
	t.Helper()

	relationshipName := "Account"
	displayName := "Account.DoesNotExist__c"
	account := &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "ExternalId__c", Type: constants.FieldTypeString},
		},
	}
	widget := &models.ObjectMetadata{
		APIName:          "Widget__c",
		Label:            "Widget",
		DisplayNameField: &displayName,
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName},
		},
	}
	repo := &lockRuleTestRepo{schemas: map[string]*models.ObjectMetadata{
		"account": account,
	}}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	err := create(svc, widget)
	if err == nil {
		t.Fatal("CreateSchema(dot-path displayNameField) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("CreateSchema(dot-path displayNameField) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "DoesNotExist__c") || !strings.Contains(err.Error(), "no case-sensitive ApiName match") {
		t.Fatalf("CreateSchema(dot-path displayNameField) err = %q, want missing child detail", err.Error())
	}
	if repo.stored != nil {
		t.Fatalf("schema was persisted despite validation failure: %#v", repo.stored)
	}
}

func TestUpdateSchemaRejectsReadSourceAuthoring(t *testing.T) {
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
	}
	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	err := svc.UpdateSchema(context.Background(), "Widget__c", &models.ObjectMetadata{
		ReadSource: &models.ObjectReadSourceConfig{
			Kind:          "physical_table",
			PhysicalTable: "_System_RecordType",
		},
	})
	if err == nil {
		t.Fatal("UpdateSchema(readSource) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(readSource) err = %T, want *ValidationError", err)
	}
	if repo.stored.ReadSource != nil {
		t.Fatalf("stored ReadSource = %+v, want unchanged nil after validation failure", repo.stored.ReadSource)
	}
}

func TestUpdateSchemaRejectsExclusiveSiblingFieldsAuthoring(t *testing.T) {
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
	}
	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	err := svc.UpdateSchema(context.Background(), "Widget__c", &models.ObjectMetadata{
		ExclusiveSiblingFields: []models.ExclusiveSiblingField{{
			FieldAPIName:          "IsDefault__c",
			PartitionFieldAPIName: "Parent__c",
		}},
	})
	if err == nil {
		t.Fatal("UpdateSchema(exclusiveSiblingFields) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(exclusiveSiblingFields) err = %T, want *ValidationError", err)
	}
	if repo.stored.ExclusiveSiblingFields != nil {
		t.Fatalf("stored ExclusiveSiblingFields = %+v, want unchanged nil after validation failure", repo.stored.ExclusiveSiblingFields)
	}
}

// TestUpdateSchemaAppliesDisplayNameField verifies that an admin PATCH to
// DisplayNameField is applied to the persisted object. This is a regression
// test for the Codex Review finding on PR #1133: UpdateSchema accepted
// displayNameField in the JSON body but silently ignored it, leaving
// _System_Object.DisplayNameField unchanged.
func TestUpdateSchemaAppliesDisplayNameField(t *testing.T) {
	ctx := context.Background()

	displayName := "AccountId"
	relationshipName := "Account"
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName},
		},
	}
	account := &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "ExternalId__c", Type: constants.FieldTypeString},
		},
	}

	repo := &lockRuleTestRepo{stored: initial, schemas: map[string]*models.ObjectMetadata{
		"widget__c": initial,
		"account":   account,
	}}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	updates := &models.ObjectMetadata{
		DisplayNameField: &displayName,
	}
	if err := svc.UpdateSchema(ctx, "Widget__c", updates); err != nil {
		t.Fatalf("UpdateSchema: %v", err)
	}

	if repo.stored.DisplayNameField == nil || *repo.stored.DisplayNameField != displayName {
		t.Errorf("DisplayNameField = %v, want %q", repo.stored.DisplayNameField, displayName)
	}
	if !repo.stored.IsCustomized {
		t.Error("IsCustomized = false, want true after admin DisplayNameField edit")
	}
}

func TestUpdateSchemaAppliesRelationshipDotPathDisplayNameField(t *testing.T) {
	ctx := context.Background()

	displayName := "Account.ExternalId__c"
	relationshipName := "Account"
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName},
		},
	}
	account := &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "ExternalId__c", Type: constants.FieldTypeString},
		},
	}

	repo := &lockRuleTestRepo{stored: initial, schemas: map[string]*models.ObjectMetadata{
		"widget__c": initial,
		"account":   account,
	}}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	updates := &models.ObjectMetadata{
		DisplayNameField: &displayName,
	}
	if err := svc.UpdateSchema(ctx, "Widget__c", updates); err != nil {
		t.Fatalf("UpdateSchema: %v", err)
	}

	if repo.stored.DisplayNameField == nil || *repo.stored.DisplayNameField != displayName {
		t.Errorf("DisplayNameField = %v, want %q", repo.stored.DisplayNameField, displayName)
	}
	if !repo.stored.IsCustomized {
		t.Error("IsCustomized = false, want true after admin DisplayNameField edit")
	}
}

func TestUpdateSchemaRejectsRelationshipDotPathDisplayNameFieldWithBadChild(t *testing.T) {
	ctx := context.Background()

	relationshipName := "Account"
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName},
		},
	}
	account := &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "ExternalId__c", Type: constants.FieldTypeString},
			{APIName: "IsActive", Type: constants.FieldTypeBoolean},
		},
	}

	tests := []struct {
		name        string
		displayName string
		want        string
	}{
		{name: "invalid_api_name", displayName: "Account.Bad Field", want: "not a legal field ApiName"},
		{name: "padded_parent", displayName: "Account .ExternalId__c", want: "padded relationship path segment"},
		{name: "padded_child", displayName: "Account. ExternalId__c", want: "padded relationship path segment"},
		{name: "missing_child", displayName: "Account.DoesNotExist__c", want: "no case-sensitive ApiName match"},
		{name: "wrong_type", displayName: "Account.IsActive", want: "not a scalar label/search type"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repo := &lockRuleTestRepo{stored: initial, schemas: map[string]*models.ObjectMetadata{
				"widget__c": initial,
				"account":   account,
			}}
			schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
			svc := NewMetadataService(repo, schemaMgr, nil)

			err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &tt.displayName})
			if err == nil {
				t.Fatal("UpdateSchema(dot-path displayNameField) = nil, want validation error")
			}
			if _, ok := err.(*apperrors.ValidationError); !ok {
				t.Fatalf("UpdateSchema(dot-path displayNameField) err = %T, want *ValidationError", err)
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("UpdateSchema(dot-path displayNameField) err = %q, want %q", err.Error(), tt.want)
			}
		})
	}
}

func TestUpdateSchemaClearsDisplayNameFieldWithExactEmptyString(t *testing.T) {
	ctx := context.Background()

	displayName := "AccountId"
	initial := &models.ObjectMetadata{
		ID:               "obj_widget",
		APIName:          "Widget__c",
		DisplayNameField: &displayName,
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	clear := ""
	if err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &clear}); err != nil {
		t.Fatalf("UpdateSchema(clear displayNameField): %v", err)
	}

	if repo.stored.DisplayNameField != nil {
		t.Fatalf("DisplayNameField = %q, want nil after exact empty-string clear", *repo.stored.DisplayNameField)
	}
	if !repo.stored.IsCustomized {
		t.Error("IsCustomized = false, want true after admin DisplayNameField clear")
	}
}

func TestUpdateSchemaRejectsWhitespaceOnlyDisplayNameField(t *testing.T) {
	ctx := context.Background()

	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	whitespace := "   "
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &whitespace})
	if err == nil {
		t.Fatal("UpdateSchema(whitespace displayNameField) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(whitespace displayNameField) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "whitespace-only") {
		t.Fatalf("UpdateSchema(whitespace displayNameField) err = %q, want whitespace-only detail", err.Error())
	}
}

func TestUpdateSchemaRejectsReferenceDisplayNameFieldWithoutRelationshipName(t *testing.T) {
	ctx := context.Background()

	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	displayName := "AccountId"
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &displayName})
	if err == nil {
		t.Fatal("UpdateSchema(reference displayNameField without relationshipName) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(reference displayNameField without relationshipName) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "relationshipName") {
		t.Fatalf("UpdateSchema(reference displayNameField without relationshipName) err = %q, want relationshipName detail", err.Error())
	}
}

func TestUpdateSchemaRejectsReferenceDisplayNameFieldWithPaddedRelationshipName(t *testing.T) {
	ctx := context.Background()

	paddedRelationshipName := " Account"
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &paddedRelationshipName},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	displayName := "AccountId"
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &displayName})
	if err == nil {
		t.Fatal("UpdateSchema(reference displayNameField with padded relationshipName) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(reference displayNameField with padded relationshipName) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "padded relationshipName") {
		t.Fatalf("UpdateSchema(reference displayNameField with padded relationshipName) err = %q, want padded relationshipName detail", err.Error())
	}
}

func TestUpdateSchemaRejectsPolymorphicReferenceDisplayNameField(t *testing.T) {
	ctx := context.Background()

	relationshipName := "What"
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "WhatId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account", "Opportunity"}, RelationshipName: &relationshipName},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	displayName := "WhatId"
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &displayName})
	if err == nil {
		t.Fatal("UpdateSchema(polymorphic reference displayNameField) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(polymorphic reference displayNameField) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "one concrete target") {
		t.Fatalf("UpdateSchema(polymorphic reference displayNameField) err = %q, want single-target detail", err.Error())
	}
}

func TestUpdateSchemaRejectsPolymorphicFlaggedReferenceDisplayNameField(t *testing.T) {
	ctx := context.Background()

	relationshipName := "Owner"
	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "OwnerId", Type: constants.FieldTypeReference, ReferenceTo: []string{"User"}, IsPolymorphic: true, RelationshipName: &relationshipName},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	displayName := "OwnerId"
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &displayName})
	if err == nil {
		t.Fatal("UpdateSchema(polymorphic-flagged reference displayNameField) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(polymorphic-flagged reference displayNameField) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "one concrete target") {
		t.Fatalf("UpdateSchema(polymorphic-flagged reference displayNameField) err = %q, want single-target detail", err.Error())
	}
}

func TestUpdateSchemaRejectsUnresolvedDisplayNameField(t *testing.T) {
	ctx := context.Background()

	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	unresolved := "NonExistent__c"
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &unresolved})
	if err == nil {
		t.Fatal("UpdateSchema(unresolved displayNameField) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(unresolved displayNameField) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "does not resolve") {
		t.Fatalf("UpdateSchema(unresolved displayNameField) err = %q, want 'does not resolve'", err.Error())
	}
}

func TestUpdateSchemaRejectsWrongTypeDisplayNameField(t *testing.T) {
	ctx := context.Background()

	initial := &models.ObjectMetadata{
		ID:      "obj_widget",
		APIName: "Widget__c",
		Fields: []models.FieldMetadata{
			{APIName: "Amount", Type: constants.FieldTypeDouble},
		},
	}

	repo := &lockRuleTestRepo{stored: initial}
	schemaMgr := NewSchemaManager(&lockRuleSchemaRepo{repo: repo})
	svc := NewMetadataService(repo, schemaMgr, nil)

	numericField := "Amount"
	err := svc.UpdateSchema(ctx, "Widget__c", &models.ObjectMetadata{DisplayNameField: &numericField})
	if err == nil {
		t.Fatal("UpdateSchema(wrong-type displayNameField) = nil, want validation error")
	}
	if _, ok := err.(*apperrors.ValidationError); !ok {
		t.Fatalf("UpdateSchema(wrong-type displayNameField) err = %T, want *ValidationError", err)
	}
	if !strings.Contains(err.Error(), "not a label-renderable type") {
		t.Fatalf("UpdateSchema(wrong-type displayNameField) err = %q, want 'not a label-renderable type'", err.Error())
	}
}

// --- stubs ---

// lockRuleTestRepo implements ports.MetadataRepository (embedded) and overrides
// the two methods UpdateSchema actually calls.
type lockRuleTestRepo struct {
	ports.MetadataRepository
	stored  *models.ObjectMetadata
	schemas map[string]*models.ObjectMetadata
}

func (r *lockRuleTestRepo) GetSchemaByAPIName(_ context.Context, apiName string) (*models.ObjectMetadata, error) {
	if r.schemas != nil {
		if schema := r.schemas[strings.ToLower(apiName)]; schema != nil {
			return schema, nil
		}
		return nil, nil
	}
	return r.stored, nil
}

func (r *lockRuleTestRepo) SaveObjectMetadata(_ context.Context, obj *models.ObjectMetadata, _ interface{}) error {
	r.stored = obj
	if r.schemas != nil {
		r.schemas[strings.ToLower(obj.APIName)] = obj
	}
	return nil
}

// lockRuleSchemaRepo implements ports.SchemaRepository (embedded) and overrides
// SaveObjectMetadata so SchemaManager.SaveObjectMetadata resolves correctly.
type lockRuleSchemaRepo struct {
	ports.SchemaRepository
	repo *lockRuleTestRepo
}

func (r *lockRuleSchemaRepo) SaveObjectMetadata(ctx context.Context, obj *models.ObjectMetadata, tx interface{}) error {
	return r.repo.SaveObjectMetadata(ctx, obj, tx)
}

// BatchSaveObjectMetadataWithCustomizationPolicy is required by the
// SchemaRepository interface; it delegates to the simple save.
func (r *lockRuleSchemaRepo) BatchSaveObjectMetadataWithCustomizationPolicy(ctx context.Context, objs []*models.ObjectMetadata, tx interface{}, _ bool) error {
	for _, o := range objs {
		if err := r.repo.SaveObjectMetadata(ctx, o, tx); err != nil {
			return err
		}
	}
	return nil
}

func (r *lockRuleSchemaRepo) MapFieldTypeToSQL(fieldType string) string {
	switch constants.NormalizeFieldType(fieldType) {
	case constants.FieldTypeReference, constants.FieldTypeMasterDetail:
		return "VARCHAR(36)"
	case constants.FieldTypeBoolean:
		return "BOOLEAN"
	case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
		return "DECIMAL(65,30)"
	default:
		return "VARCHAR(255)"
	}
}

// PrepareFieldForBatch satisfies the SchemaRepository interface; not exercised
// in this test.
func (r *lockRuleSchemaRepo) PrepareFieldForBatch(_ string, col domainschema.ColumnDefinition) domainschema.FieldWithContext {
	return domainschema.FieldWithContext{}
}
