package metadata

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"testing"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/domain/schema"
	"github.com/nexuscrm/backend/pkg/formula"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ============================================================================
// Mock MetadataRepository
// ============================================================================

type mockMetadataRepo struct {
	// Schema
	schemas          map[string]*models.ObjectMetadata
	getAllSchemasErr error

	// Flow definitions
	flowDefinitions          map[string]*models.FlowDefinition
	flowVersions             map[string]*models.FlowVersion
	markedFlowDefs           []string
	patchFlowDefinitionCalls []flowDefinitionPatchCall
	getAllFlowDefsErr        error
	getFlowVersionsErr       error

	// Validation rules
	validationRules    map[string][]*models.ValidationRule
	validationRuleByID map[string]*models.ValidationRule

	// Auto-numbers
	autoNumbers map[string][]*models.AutoNumber

	// Identifier rules (Unified Identifier Engine)
	// All rows here are treated as IsActive=1 by GetAllIdentifierRules unless
	// inactiveIdentifierRules contains the rule's ID — that lets tests assert
	// that inactive rules never reach the cache.
	identifierRules          []*models.IdentifierRule
	inactiveIdentifierRules  map[string]bool // ruleID -> true means filtered out by repo
	identifierRuleQueryCount int             // counts GetAllIdentifierRules calls (cache-hit assertions)
	getAllIdentifierRulesErr error

	// Layouts
	layouts                         map[string][]*models.PageLayout // key: objectAPIName
	layoutByID                      map[string]*models.PageLayout
	assignmentsByObject             map[string][]*models.LayoutAssignment
	deletedAssignmentIDs            []string
	setDefaultLayoutCalls           []defaultLayoutCall
	profileLayouts                  map[string]string // key: profileID+objectAPIName -> layoutID
	relatedListCfgs                 map[string][]ports.RelatedListConfig
	disableImplicitLayoutAssignment bool

	// List views
	listViews     map[string][]*models.ListView // key: objectAPIName
	listViewCount map[string]int

	// Actions
	actions      map[string]*models.ActionMetadata
	actionsByObj map[string][]*models.ActionMetadata
	allActions   []*models.ActionMetadata

	// Sharing rules
	sharingRules map[string][]*models.SharingRule

	// Relationships
	relationships      map[string][]*models.Relationship
	fieldDependencies  map[string][]*models.FieldDependency
	childRelationships map[string][]*models.ObjectMetadata

	// Record types
	recordTypes map[string][]*models.RecordType

	// Error overrides for testing error paths
	getSchemaErr                 error
	createFlowErr                error
	updateFlowErr                error
	deleteFlowErr                error
	createFlowVersionErr         error
	updateFlowVersionErr         error
	deleteFlowVersionErr         error
	activateFlowVersionErr       error
	deactivateFlowVersionErr     error
	setLatestVersionIdErr        error
	setTriggerInfoErr            error
	createListViewErr            error
	updateListViewErr            error
	deleteListViewErr            error
	saveLayoutErr                error
	deleteLayoutErr              error
	assignLayoutErr              error
	createActionErr              error
	updateActionErr              error
	deleteActionErr              error
	checkActionExistsErr         error
	createValidationRuleErr      error
	updateValidationRuleErr      error
	deleteValidationRuleErr      error
	checkValidationRuleExistsErr error
	getValidationRulesErr        error
	upsertAutoNumberErr          error
	countListViewsErr            error

	// Approval layouts
	approvalLayouts         map[string][]*models.ApprovalPageLayout // key: objectAPIName
	approvalLayoutByID      map[string]*models.ApprovalPageLayout
	deleteApprovalLayoutErr error
	saveApprovalLayoutErr   error
	triggerInfoUpdates      []triggerInfoUpdate
}

type defaultLayoutCall struct {
	objectAPIName string
	layoutID      string
}

type flowDefinitionPatchCall struct {
	id    string
	patch models.FlowDefinitionPatch
}

type triggerInfoUpdate struct {
	definitionId  string
	triggerType   string
	objectOrEvent string
}

// Ensure interface compliance
var _ ports.MetadataRepository = (*mockMetadataRepo)(nil)

func newMockMetadataRepo() *mockMetadataRepo {
	return &mockMetadataRepo{
		schemas:                 make(map[string]*models.ObjectMetadata),
		flowDefinitions:         make(map[string]*models.FlowDefinition),
		flowVersions:            make(map[string]*models.FlowVersion),
		validationRules:         make(map[string][]*models.ValidationRule),
		validationRuleByID:      make(map[string]*models.ValidationRule),
		autoNumbers:             make(map[string][]*models.AutoNumber),
		identifierRules:         []*models.IdentifierRule{},
		inactiveIdentifierRules: make(map[string]bool),
		layouts:                 make(map[string][]*models.PageLayout),
		layoutByID:              make(map[string]*models.PageLayout),
		assignmentsByObject:     make(map[string][]*models.LayoutAssignment),
		profileLayouts:          make(map[string]string),
		relatedListCfgs:         make(map[string][]ports.RelatedListConfig),
		listViews:               make(map[string][]*models.ListView),
		listViewCount:           make(map[string]int),
		actions:                 make(map[string]*models.ActionMetadata),
		actionsByObj:            make(map[string][]*models.ActionMetadata),
		allActions:              []*models.ActionMetadata{},
		sharingRules:            make(map[string][]*models.SharingRule),
		relationships:           make(map[string][]*models.Relationship),
		fieldDependencies:       make(map[string][]*models.FieldDependency),
		childRelationships:      make(map[string][]*models.ObjectMetadata),
		recordTypes:             make(map[string][]*models.RecordType),
	}
}

func (m *mockMetadataRepo) GetSchemaByAPIName(_ context.Context, apiName string) (*models.ObjectMetadata, error) {
	if m.getSchemaErr != nil {
		return nil, m.getSchemaErr
	}
	s, ok := m.schemas[strings.ToLower(apiName)]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return s, nil
}

func (m *mockMetadataRepo) GetAllSchemas(_ context.Context) ([]*models.ObjectMetadata, error) {
	if m.getAllSchemasErr != nil {
		return nil, m.getAllSchemasErr
	}
	result := make([]*models.ObjectMetadata, 0, len(m.schemas))
	for _, s := range m.schemas {
		result = append(result, s)
	}
	return result, nil
}

func (m *mockMetadataRepo) GetFieldsForObject(_ context.Context, objectID string) ([]models.FieldMetadata, error) {
	for _, s := range m.schemas {
		if s.ID == objectID {
			return s.Fields, nil
		}
	}
	return nil, fmt.Errorf("not found")
}

func (m *mockMetadataRepo) GetRecordTypes(_ context.Context, objectAPIName string) ([]*models.RecordType, error) {
	return m.recordTypes[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) GetAutoNumbers(_ context.Context, objectAPIName string) ([]*models.AutoNumber, error) {
	return m.autoNumbers[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) GetAllAutoNumbers(_ context.Context) ([]*models.AutoNumber, error) {
	var all []*models.AutoNumber
	for _, ans := range m.autoNumbers {
		all = append(all, ans...)
	}
	return all, nil
}

// GetAllIdentifierRules mirrors the real repo's behavior:
//   - filters IsActive=0 rows at the SQL layer (tests can flip a rule to
//     inactive via inactiveIdentifierRules and expect it not to appear);
//   - returns rows ORDER BY (ObjectApiName, RuleName) ASC, so callers can
//     rely on lex-smallest RuleName winning when duplicates exist.
func (m *mockMetadataRepo) GetAllIdentifierRules(_ context.Context) ([]*models.IdentifierRule, error) {
	m.identifierRuleQueryCount++
	if m.getAllIdentifierRulesErr != nil {
		return nil, m.getAllIdentifierRulesErr
	}
	out := make([]*models.IdentifierRule, 0, len(m.identifierRules))
	for _, r := range m.identifierRules {
		if m.inactiveIdentifierRules[r.ID] {
			continue
		}
		// Real repo materializes IsActive=true for everything it returns
		// because the WHERE clause filters IsActive=1 only.
		copy := *r
		copy.IsActive = true
		out = append(out, &copy)
	}
	// Match the SQL ORDER BY (ObjectApiName, RuleName) ASC contract.
	sort.SliceStable(out, func(i, j int) bool {
		if out[i].ObjectAPIName != out[j].ObjectAPIName {
			return out[i].ObjectAPIName < out[j].ObjectAPIName
		}
		return out[i].RuleName < out[j].RuleName
	})
	return out, nil
}

func (m *mockMetadataRepo) GetAllValidationRules(_ context.Context) ([]*models.ValidationRule, error) {
	var all []*models.ValidationRule
	for _, rules := range m.validationRules {
		all = append(all, rules...)
	}
	return all, nil
}

func (m *mockMetadataRepo) GetRelationships(_ context.Context, childObjectAPIName string) ([]*models.Relationship, error) {
	return m.relationships[strings.ToLower(childObjectAPIName)], nil
}

func (m *mockMetadataRepo) GetAllRelationships(_ context.Context) ([]*models.Relationship, error) {
	var all []*models.Relationship
	for _, relationships := range m.relationships {
		all = append(all, relationships...)
	}
	return all, nil
}

func (m *mockMetadataRepo) GetFieldDependencies(_ context.Context, objectAPIName string) ([]*models.FieldDependency, error) {
	return m.fieldDependencies[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) GetChildRelationships(_ context.Context, parentObjectAPIName string) ([]*models.ObjectMetadata, error) {
	return m.childRelationships[strings.ToLower(parentObjectAPIName)], nil
}

func (m *mockMetadataRepo) ConfigureAutoNumberField(_ context.Context, _, _, _ string, _ int64) error {
	return m.upsertAutoNumberErr
}

// UI & Layouts
func (m *mockMetadataRepo) UpsertUIComponent(_ context.Context, _ *models.UIComponent) error {
	return nil
}
func (m *mockMetadataRepo) UpsertSetupPage(_ context.Context, _ *models.SetupPage) error { return nil }
func (m *mockMetadataRepo) GetSetupPages(_ context.Context) ([]*models.SetupPage, error) {
	return nil, nil
}

func (m *mockMetadataRepo) UpsertLayout(_ context.Context, layout *models.PageLayout) error {
	if m.saveLayoutErr != nil {
		return m.saveLayoutErr
	}
	m.storeLayout(layout)
	return nil
}

func (m *mockMetadataRepo) SaveLayout(_ context.Context, layout *models.PageLayout) error {
	if m.saveLayoutErr != nil {
		return m.saveLayoutErr
	}
	m.storeLayout(layout)
	return nil
}
func (m *mockMetadataRepo) MarkLayoutCustomized(_ context.Context, _ string) {}
func (m *mockMetadataRepo) MarkFlowDefinitionCustomized(_ context.Context, id string) error {
	m.markedFlowDefs = append(m.markedFlowDefs, id)
	if def, ok := m.flowDefinitions[id]; ok {
		def.IsCustomized = true
	}
	return nil
}

func (m *mockMetadataRepo) GetLayout(_ context.Context, id string) (*models.PageLayout, error) {
	l, ok := m.layoutByID[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return l, nil
}

func (m *mockMetadataRepo) GetLayouts(_ context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	return append([]*models.PageLayout(nil), m.layouts[strings.ToLower(objectAPIName)]...), nil
}

func (m *mockMetadataRepo) DeleteLayout(_ context.Context, id string) error {
	if m.deleteLayoutErr != nil {
		return m.deleteLayoutErr
	}
	delete(m.layoutByID, id)
	return nil
}

func (m *mockMetadataRepo) AssignLayoutToProfile(_ context.Context, profileID, objectAPIName, layoutID string) error {
	if m.assignLayoutErr != nil {
		return m.assignLayoutErr
	}
	m.profileLayouts[profileID+objectAPIName] = layoutID
	return nil
}

func (m *mockMetadataRepo) GetLayoutIDForProfile(_ context.Context, objectAPIName, profileID string) (string, error) {
	id, ok := m.profileLayouts[profileID+objectAPIName]
	if !ok {
		return "", fmt.Errorf("not found")
	}
	return id, nil
}

func (m *mockMetadataRepo) GetRelatedListConfigs(_ context.Context, layoutObjectAPIName string) ([]ports.RelatedListConfig, error) {
	return m.relatedListCfgs[strings.ToLower(layoutObjectAPIName)], nil
}

// Themes
func (m *mockMetadataRepo) GetActiveTheme(_ context.Context) (*models.Theme, error) { return nil, nil }
func (m *mockMetadataRepo) GetThemeByName(_ context.Context, _ string) (*models.Theme, error) {
	return nil, nil
}
func (m *mockMetadataRepo) CreateTheme(_ context.Context, _ *models.Theme) error { return nil }
func (m *mockMetadataRepo) UpdateTheme(_ context.Context, _ *models.Theme) error { return nil }
func (m *mockMetadataRepo) ActivateTheme(_ context.Context, _ string) error      { return nil }

// Apps
func (m *mockMetadataRepo) GetAllApps(_ context.Context) ([]*models.AppConfig, error) {
	return nil, nil
}
func (m *mockMetadataRepo) GetApp(_ context.Context, _ string) (*models.AppConfig, error) {
	return nil, nil
}
func (m *mockMetadataRepo) GetAppWithTx(_ context.Context, _ interface{}, _ string) (*models.AppConfig, error) {
	return nil, nil
}
func (m *mockMetadataRepo) CreateApp(_ context.Context, _ *models.AppConfig) error { return nil }
func (m *mockMetadataRepo) CreateAppMenuItem(_ context.Context, _ *models.AppConfig) error {
	return nil
}
func (m *mockMetadataRepo) UpdateApp(_ context.Context, _ string, _ *models.AppConfig) error {
	return nil
}
func (m *mockMetadataRepo) UpdateAppWithTx(_ context.Context, _ interface{}, _ string, _ *models.AppConfig) error {
	return nil
}
func (m *mockMetadataRepo) DeleteApp(_ context.Context, _ string) error { return nil }

// Dashboards
func (m *mockMetadataRepo) GetAllDashboards(_ context.Context) ([]*models.DashboardConfig, error) {
	return nil, nil
}
func (m *mockMetadataRepo) GetDashboard(_ context.Context, _ string) (*models.DashboardConfig, error) {
	return nil, nil
}
func (m *mockMetadataRepo) CreateDashboard(_ context.Context, _ *models.DashboardConfig) error {
	return nil
}
func (m *mockMetadataRepo) UpdateDashboard(_ context.Context, _ string, _ *models.DashboardConfig) error {
	return nil
}
func (m *mockMetadataRepo) DeleteDashboard(_ context.Context, _ string) error { return nil }

// List Views
func (m *mockMetadataRepo) GetListViews(_ context.Context, objectAPIName string) ([]*models.ListView, error) {
	return m.listViews[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) CreateListView(_ context.Context, lv *models.ListView) error {
	if m.createListViewErr != nil {
		return m.createListViewErr
	}
	key := strings.ToLower(lv.ObjectAPIName)
	m.listViews[key] = append(m.listViews[key], lv)
	m.listViewCount[key]++
	return nil
}

func (m *mockMetadataRepo) UpdateListView(_ context.Context, _ string, _ *models.ListView) error {
	return m.updateListViewErr
}

func (m *mockMetadataRepo) DeleteListView(_ context.Context, _ string) error {
	return m.deleteListViewErr
}

func (m *mockMetadataRepo) CountListViews(_ context.Context, objectAPIName string) (int, error) {
	if m.countListViewsErr != nil {
		return 0, m.countListViewsErr
	}
	return m.listViewCount[strings.ToLower(objectAPIName)], nil
}

// Actions
func (m *mockMetadataRepo) GetActions(_ context.Context, objectAPIName string) ([]*models.ActionMetadata, error) {
	return m.actionsByObj[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) GetAction(_ context.Context, id string) (*models.ActionMetadata, error) {
	a, ok := m.actions[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return a, nil
}

func (m *mockMetadataRepo) GetAllActions(_ context.Context) ([]*models.ActionMetadata, error) {
	return m.allActions, nil
}

func (m *mockMetadataRepo) CreateAction(_ context.Context, action *models.ActionMetadata) error {
	if m.createActionErr != nil {
		return m.createActionErr
	}
	m.actions[action.ID] = action
	return nil
}

func (m *mockMetadataRepo) UpdateAction(_ context.Context, actionID string, updates *models.ActionMetadata) error {
	if m.updateActionErr != nil {
		return m.updateActionErr
	}
	m.actions[actionID] = updates
	return nil
}

func (m *mockMetadataRepo) DeleteAction(_ context.Context, actionID string) error {
	if m.deleteActionErr != nil {
		return m.deleteActionErr
	}
	delete(m.actions, actionID)
	return nil
}

func (m *mockMetadataRepo) CheckActionExists(_ context.Context, objectAPIName, name string) (bool, error) {
	if m.checkActionExistsErr != nil {
		return false, m.checkActionExistsErr
	}
	for _, a := range m.actions {
		if a.ObjectAPIName == objectAPIName && a.Name == name {
			return true, nil
		}
	}
	return false, nil
}

func (m *mockMetadataRepo) GetSharingRules(_ context.Context, objectAPIName string) ([]*models.SharingRule, error) {
	return m.sharingRules[strings.ToLower(objectAPIName)], nil
}

// Flow Definitions
func (m *mockMetadataRepo) GetAllFlowDefinitions(_ context.Context) ([]*models.FlowDefinition, error) {
	if m.getAllFlowDefsErr != nil {
		return nil, m.getAllFlowDefsErr
	}
	result := make([]*models.FlowDefinition, 0, len(m.flowDefinitions))
	for _, fd := range m.flowDefinitions {
		result = append(result, fd)
	}
	return result, nil
}

func (m *mockMetadataRepo) GetFlowDefinition(_ context.Context, id string) (*models.FlowDefinition, error) {
	fd, ok := m.flowDefinitions[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return fd, nil
}

func (m *mockMetadataRepo) GetFlowDefinitionByName(_ context.Context, developerName string) (*models.FlowDefinition, error) {
	for _, fd := range m.flowDefinitions {
		if fd.DeveloperName == developerName {
			return fd, nil
		}
	}
	return nil, fmt.Errorf("not found")
}

func (m *mockMetadataRepo) CreateFlowDefinition(_ context.Context, def *models.FlowDefinition) error {
	if m.createFlowErr != nil {
		return m.createFlowErr
	}
	m.flowDefinitions[def.Id] = def
	return nil
}

func (m *mockMetadataRepo) UpdateFlowDefinition(_ context.Context, id string, def *models.FlowDefinition) error {
	if m.updateFlowErr != nil {
		return m.updateFlowErr
	}
	m.flowDefinitions[id] = def
	return nil
}

func (m *mockMetadataRepo) PatchFlowDefinition(_ context.Context, id string, patch *models.FlowDefinitionPatch) error {
	if m.updateFlowErr != nil {
		return m.updateFlowErr
	}
	if patch == nil {
		return nil
	}
	copyPatch := *patch
	m.patchFlowDefinitionCalls = append(m.patchFlowDefinitionCalls, flowDefinitionPatchCall{
		id:    id,
		patch: copyPatch,
	})
	def, ok := m.flowDefinitions[id]
	if !ok {
		def = &models.FlowDefinition{Id: id}
		m.flowDefinitions[id] = def
	}
	if patch.DeveloperName != nil {
		def.DeveloperName = *patch.DeveloperName
	}
	if patch.MasterLabel != nil {
		def.MasterLabel = *patch.MasterLabel
	}
	if patch.Description != nil {
		def.Description = *patch.Description
	}
	if patch.ProcessType != nil {
		def.ProcessType = *patch.ProcessType
	}
	if patch.TriggerType != nil {
		def.TriggerType = *patch.TriggerType
	}
	if patch.TriggerObjectOrEvent != nil {
		def.TriggerObjectOrEvent = *patch.TriggerObjectOrEvent
	}
	if patch.ActiveVersionId != nil {
		def.ActiveVersionId = *patch.ActiveVersionId
	}
	if patch.LatestVersionId != nil {
		def.LatestVersionId = *patch.LatestVersionId
	}
	return nil
}

func (m *mockMetadataRepo) SetLatestVersionId(_ context.Context, _, _ string) error {
	return m.setLatestVersionIdErr
}

func (m *mockMetadataRepo) SetTriggerInfo(_ context.Context, definitionId, triggerType, objectOrEvent string) error {
	if m.setTriggerInfoErr != nil {
		return m.setTriggerInfoErr
	}
	m.triggerInfoUpdates = append(m.triggerInfoUpdates, triggerInfoUpdate{
		definitionId:  definitionId,
		triggerType:   triggerType,
		objectOrEvent: objectOrEvent,
	})
	if def, ok := m.flowDefinitions[definitionId]; ok {
		def.TriggerType = triggerType
		def.TriggerObjectOrEvent = objectOrEvent
	}
	return nil
}

func (m *mockMetadataRepo) SetDefaultLayout(_ context.Context, objectAPIName, layoutID string, actorID *string) error {
	key := strings.ToLower(objectAPIName)
	m.setDefaultLayoutCalls = append(m.setDefaultLayoutCalls, defaultLayoutCall{
		objectAPIName: objectAPIName,
		layoutID:      layoutID,
	})

	for _, layout := range m.layouts[key] {
		layout.IsDefault = layout.ID == layoutID
	}
	if layout, ok := m.layoutByID[layoutID]; ok {
		layout.IsDefault = true
	}

	assignment := &models.LayoutAssignment{
		ObjectAPIName: objectAPIName,
		LayoutID:      layoutID,
	}
	if actorID != nil && strings.TrimSpace(*actorID) != "" {
		assignment.CreatedByID = actorID
		routeSource := models.LayoutAssignmentRouteSourceAdminAuthored
		assignment.RouteSource = &routeSource
	}
	assignment.ComputeSpecificity()
	m.upsertAssignment(assignment)
	return nil
}

func (m *mockMetadataRepo) DeleteFlowDefinition(_ context.Context, id string) error {
	if m.deleteFlowErr != nil {
		return m.deleteFlowErr
	}
	delete(m.flowDefinitions, id)
	return nil
}

// Flow Versions
func (m *mockMetadataRepo) GetFlowVersions(_ context.Context, definitionId string) ([]*models.FlowVersion, error) {
	if m.getFlowVersionsErr != nil {
		return nil, m.getFlowVersionsErr
	}
	var result []*models.FlowVersion
	for _, v := range m.flowVersions {
		if v.DefinitionId == definitionId {
			result = append(result, v)
		}
	}
	return result, nil
}

func (m *mockMetadataRepo) GetFlowVersionsByIDs(_ context.Context, ids []string) (map[string]*models.FlowVersion, error) {
	result := make(map[string]*models.FlowVersion)
	for _, id := range ids {
		if v, ok := m.flowVersions[id]; ok {
			result[id] = v
		}
	}
	return result, nil
}
func (m *mockMetadataRepo) GetFlowVersion(_ context.Context, id string) (*models.FlowVersion, error) {
	v, ok := m.flowVersions[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return v, nil
}

func (m *mockMetadataRepo) GetActiveFlowVersion(_ context.Context, _ string) (*models.FlowVersion, error) {
	return nil, nil
}

func (m *mockMetadataRepo) CreateFlowVersion(_ context.Context, version *models.FlowVersion) error {
	if m.createFlowVersionErr != nil {
		return m.createFlowVersionErr
	}
	copy := *version
	copy.Source = flowSourceCustom
	m.flowVersions[version.Id] = &copy
	return nil
}

func (m *mockMetadataRepo) CreateSeededFlowVersion(_ context.Context, version *models.FlowVersion) error {
	if m.createFlowVersionErr != nil {
		return m.createFlowVersionErr
	}
	copy := *version
	copy.Source = "standard"
	m.flowVersions[version.Id] = &copy
	return nil
}

func (m *mockMetadataRepo) CreateTenantFlowVersion(_ context.Context, version *models.FlowVersion) error {
	if m.createFlowVersionErr != nil {
		return m.createFlowVersionErr
	}
	copy := *version
	copy.Source = flowSourceCustom
	m.flowVersions[version.Id] = &copy
	return nil
}

func (m *mockMetadataRepo) UpdateFlowVersion(_ context.Context, id string, version *models.FlowVersion) error {
	if m.updateFlowVersionErr != nil {
		return m.updateFlowVersionErr
	}
	m.flowVersions[id] = version
	return nil
}

func (m *mockMetadataRepo) ActivateFlowVersion(_ context.Context, _ string) error {
	return m.activateFlowVersionErr
}

func (m *mockMetadataRepo) DeactivateFlowVersion(_ context.Context, _ string) error {
	return m.deactivateFlowVersionErr
}

func (m *mockMetadataRepo) DeleteFlowVersion(_ context.Context, id string) error {
	if m.deleteFlowVersionErr != nil {
		return m.deleteFlowVersionErr
	}
	delete(m.flowVersions, id)
	return nil
}

// Flow Interviews
func (m *mockMetadataRepo) GetFlowInterviews(_ context.Context) ([]*models.FlowInterview, error) {
	return nil, nil
}
func (m *mockMetadataRepo) GetFlowInterview(_ context.Context, _ string) (*models.FlowInterview, error) {
	return nil, nil
}
func (m *mockMetadataRepo) SaveFlowInterview(_ context.Context, _ *models.FlowInterview) error {
	return nil
}
func (m *mockMetadataRepo) DeleteFlowInterview(_ context.Context, _ string) error { return nil }

// Validation Rules
func (m *mockMetadataRepo) GetValidationRules(_ context.Context, objectAPIName string) ([]*models.ValidationRule, error) {
	if m.getValidationRulesErr != nil {
		return nil, m.getValidationRulesErr
	}
	return m.validationRules[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) GetValidationRule(_ context.Context, id string) (*models.ValidationRule, error) {
	r, ok := m.validationRuleByID[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return r, nil
}

func (m *mockMetadataRepo) CreateValidationRule(_ context.Context, rule *models.ValidationRule) error {
	if m.createValidationRuleErr != nil {
		return m.createValidationRuleErr
	}
	m.validationRuleByID[rule.ID] = rule
	key := strings.ToLower(rule.ObjectAPIName)
	m.validationRules[key] = append(m.validationRules[key], rule)
	return nil
}

func (m *mockMetadataRepo) UpdateValidationRule(_ context.Context, id string, updates *models.ValidationRule) error {
	if m.updateValidationRuleErr != nil {
		return m.updateValidationRuleErr
	}
	m.validationRuleByID[id] = updates
	return nil
}

func (m *mockMetadataRepo) DeleteValidationRule(_ context.Context, id string) error {
	if m.deleteValidationRuleErr != nil {
		return m.deleteValidationRuleErr
	}
	delete(m.validationRuleByID, id)
	return nil
}

func (m *mockMetadataRepo) CheckValidationRuleExists(_ context.Context, objectAPIName, name string) (bool, error) {
	if m.checkValidationRuleExistsErr != nil {
		return false, m.checkValidationRuleExistsErr
	}
	for _, r := range m.validationRuleByID {
		if strings.ToLower(r.ObjectAPIName) == strings.ToLower(objectAPIName) && r.Name == name {
			return true, nil
		}
	}
	return false, nil
}

// ============================================================================
// Mock SchemaRepository
// ============================================================================

type mockSchemaRepo struct {
	// Tracking
	createdTables   []string
	droppedTables   []string
	addedColumns    map[string][]schema.ColumnDefinition
	droppedColumns  map[string][]string
	modifiedColumns map[string][]schema.ColumnDefinition
	savedObjects    []*models.ObjectMetadata
	savedFields     []schema.FieldWithContext

	// Error overrides
	createTableErr        error
	createWithMetadataErr error
	dropTableErr          error
	addColumnErr          error
	dropColumnErr         error
	modifyColumnErr       error
	saveObjectErr         error
	insertObjectErr       error
	saveFieldErr          error
	batchSaveFieldErr     error
	batchAddColumnsErr    error
	validateFormulaErr    error
}

var _ ports.SchemaRepository = (*mockSchemaRepo)(nil)

func newMockSchemaRepo() *mockSchemaRepo {
	return &mockSchemaRepo{
		addedColumns:    make(map[string][]schema.ColumnDefinition),
		droppedColumns:  make(map[string][]string),
		modifiedColumns: make(map[string][]schema.ColumnDefinition),
	}
}

func (m *mockSchemaRepo) CreatePhysicalTable(_ context.Context, def schema.TableDefinition) error {
	if m.createTableErr != nil {
		return m.createTableErr
	}
	m.createdTables = append(m.createdTables, def.TableName)
	return nil
}

func (m *mockSchemaRepo) CreateTableWithStrictMetadata(_ context.Context, def schema.TableDefinition, _ *models.ObjectMetadata) error {
	if m.createWithMetadataErr != nil {
		return m.createWithMetadataErr
	}
	m.createdTables = append(m.createdTables, def.TableName)
	return nil
}

func (m *mockSchemaRepo) BatchCreatePhysicalTables(_ context.Context, defs []schema.TableDefinition) error {
	for _, d := range defs {
		m.createdTables = append(m.createdTables, d.TableName)
	}
	return nil
}

func (m *mockSchemaRepo) DropTable(tableName string) error {
	if m.dropTableErr != nil {
		return m.dropTableErr
	}
	m.droppedTables = append(m.droppedTables, tableName)
	return nil
}

func (m *mockSchemaRepo) ValidateSchema(_ string) error                           { return nil }
func (m *mockSchemaRepo) ValidateFieldDefinition(_ schema.ColumnDefinition) error { return nil }
func (m *mockSchemaRepo) ValidateSchemaRegistry() (*schema.SchemaHealth, error) {
	return &schema.SchemaHealth{}, nil
}

func (m *mockSchemaRepo) BatchRegisterTables(_ []schema.TableDefinition, _ interface{}) error {
	return nil
}

func (m *mockSchemaRepo) AddColumn(tableName string, col schema.ColumnDefinition) error {
	if m.addColumnErr != nil {
		return m.addColumnErr
	}
	m.addedColumns[tableName] = append(m.addedColumns[tableName], col)
	return nil
}

func (m *mockSchemaRepo) RegisterFieldOnly(_ string, _ schema.ColumnDefinition) error {
	return nil
}

func (m *mockSchemaRepo) EnsureColumn(tableName string, col schema.ColumnDefinition) error {
	return m.AddColumn(tableName, col)
}

func (m *mockSchemaRepo) DropColumn(tableName, colName string) error {
	if m.dropColumnErr != nil {
		return m.dropColumnErr
	}
	m.droppedColumns[tableName] = append(m.droppedColumns[tableName], colName)
	return nil
}

func (m *mockSchemaRepo) ModifyColumn(tableName, _ string, col schema.ColumnDefinition) error {
	if m.modifyColumnErr != nil {
		return m.modifyColumnErr
	}
	m.modifiedColumns[tableName] = append(m.modifiedColumns[tableName], col)
	return nil
}

func (m *mockSchemaRepo) IsSystemColumn(name string) bool {
	return constants.IsSystemField(name)
}

func (m *mockSchemaRepo) GetStandardSystemColumns() []schema.ColumnDefinition {
	return []schema.ColumnDefinition{
		{Name: "Id", Type: "VARCHAR(36)", PrimaryKey: true, LogicalType: "string"},
		{Name: "CreatedDate", Type: "DATETIME", LogicalType: "datetime"},
		{Name: "LastModifiedDate", Type: "DATETIME", LogicalType: "datetime"},
		{Name: "OwnerId", Type: "VARCHAR(36)", LogicalType: "reference"},
	}
}

func (m *mockSchemaRepo) GetStandardFieldMetadata() []models.FieldMetadata {
	return []models.FieldMetadata{
		{APIName: "Id", Label: "Record ID", Type: constants.FieldTypeString, IsSystem: true, Required: true},
		{APIName: "CreatedDate", Label: "Created Date", Type: constants.FieldTypeDatetime, IsSystem: true},
		{APIName: "LastModifiedDate", Label: "Last Modified Date", Type: constants.FieldTypeDatetime, IsSystem: true},
		{APIName: "OwnerId", Label: "Owner ID", Type: constants.FieldTypeReference, IsSystem: true},
	}
}

func (m *mockSchemaRepo) SaveObjectMetadata(_ context.Context, obj *models.ObjectMetadata, _ interface{}) error {
	if m.saveObjectErr != nil {
		return m.saveObjectErr
	}
	m.savedObjects = append(m.savedObjects, obj)
	return nil
}

func (m *mockSchemaRepo) InsertObjectMetadata(_ context.Context, obj *models.ObjectMetadata, _ interface{}) error {
	if m.insertObjectErr != nil {
		return m.insertObjectErr
	}
	m.savedObjects = append(m.savedObjects, obj)
	return nil
}

func (m *mockSchemaRepo) BatchSaveObjectMetadata(_ context.Context, objs []*models.ObjectMetadata, _ interface{}) error {
	m.savedObjects = append(m.savedObjects, objs...)
	return nil
}

func (m *mockSchemaRepo) SaveFieldMetadataWithIDs(_ context.Context, field *models.FieldMetadata, objectID, _ /* objectAPIName */, fieldID string, _ interface{}) error {
	if m.saveFieldErr != nil {
		return m.saveFieldErr
	}
	m.savedFields = append(m.savedFields, schema.FieldWithContext{
		ObjectID: objectID,
		FieldID:  fieldID,
		Field:    field,
	})
	return nil
}

func (m *mockSchemaRepo) BatchSaveFieldMetadata(_ context.Context, fields []schema.FieldWithContext, _ interface{}) error {
	if m.batchSaveFieldErr != nil {
		return m.batchSaveFieldErr
	}
	m.savedFields = append(m.savedFields, fields...)
	return nil
}

func (m *mockSchemaRepo) PrepareFieldForBatch(tableName string, col schema.ColumnDefinition) schema.FieldWithContext {
	return schema.FieldWithContext{
		ObjectID: tableName,
		FieldID:  col.Name,
		Field:    &models.FieldMetadata{APIName: col.Name},
	}
}

func (m *mockSchemaRepo) GetTableRegistry(_ context.Context) ([]*schema.TableRegistryItem, error) {
	return nil, nil
}

func (m *mockSchemaRepo) MapFieldTypeToSQL(fieldType string) string {
	switch fieldType {
	case "string":
		return "VARCHAR(255)"
	case "double", "currency", "percent":
		return "DOUBLE"
	case "boolean":
		return "TINYINT(1)"
	case "date":
		return "DATE"
	case "datetime":
		return "DATETIME"
	case "reference":
		return "VARCHAR(36)"
	default:
		return "TEXT"
	}
}

func (m *mockSchemaRepo) ValidateFormula(_ string, _ map[string]interface{}) error {
	return m.validateFormulaErr
}

func (m *mockSchemaRepo) BatchAddColumns(tableName string, fields []models.FieldMetadata) error {
	if m.batchAddColumnsErr != nil {
		return m.batchAddColumnsErr
	}
	for _, f := range fields {
		m.addedColumns[tableName] = append(m.addedColumns[tableName], schema.ColumnDefinition{
			Name:        f.APIName,
			LogicalType: string(f.Type),
		})
	}
	return nil
}

// ============================================================================
// Mock EventPublisher
// ============================================================================

type mockEventPublisher struct {
	published  []publishedEvent
	publishErr error
}

type publishedEvent struct {
	eventType sharedEvents.EventType
	payload   interface{}
}

var _ ports.EventPublisher = (*mockEventPublisher)(nil)

func newMockEventPublisher() *mockEventPublisher {
	return &mockEventPublisher{}
}

func (m *mockEventPublisher) Subscribe(_ sharedEvents.EventType, _ ports.EventHandler) func() {
	return func() {}
}

func (m *mockEventPublisher) Publish(_ context.Context, eventType sharedEvents.EventType, payload interface{}) error {
	if m.publishErr != nil {
		return m.publishErr
	}
	m.published = append(m.published, publishedEvent{eventType: eventType, payload: payload})
	return nil
}

func (m *mockEventPublisher) PublishAsyncWithContext(_ context.Context, eventType sharedEvents.EventType, payload interface{}) {
	m.published = append(m.published, publishedEvent{eventType: eventType, payload: payload})
}

// ============================================================================
// Test Helper: create a MetadataService with mocks
// ============================================================================

func newTestService() (*MetadataService, *mockMetadataRepo, *mockSchemaRepo, *mockEventPublisher) {
	metaRepo := newMockMetadataRepo()
	schemaRepo := newMockSchemaRepo()
	eventPub := newMockEventPublisher()
	schemaMgr := NewSchemaManager(schemaRepo)
	ms := NewMetadataService(metaRepo, schemaMgr, eventPub)
	return ms, metaRepo, schemaRepo, eventPub
}

func assertMarkedFlowDefinitions(t *testing.T, repo *mockMetadataRepo, want ...string) {
	t.Helper()
	assert.Equal(t, want, repo.markedFlowDefs)
}

// ============================================================================
// Tests: Schema CRUD
// ============================================================================

func TestMetadataService_CreateSchema_HappyPath(t *testing.T) {
	ms, metaRepo, schemaRepo, eventPub := newTestService()
	ctx := context.Background()

	// No existing schema
	schema := &models.ObjectMetadata{
		APIName:     "TestAccount",
		Label:       "Test Account",
		PluralLabel: "Test Accounts",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	err := ms.CreateSchema(ctx, schema)
	require.NoError(t, err)

	// Verify table was created
	assert.Contains(t, schemaRepo.createdTables, "TestAccount")

	// Verify layout was upserted
	assert.NotEmpty(t, metaRepo.layoutByID)

	// Verify event published
	assert.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ObjectCreated, eventPub.published[0].eventType)
}

func TestMetadataService_CreateSchema_DuplicateAPIName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	// Pre-populate a schema
	metaRepo.schemas["testaccount"] = &models.ObjectMetadata{
		APIName: "TestAccount",
		Label:   "Test Account",
	}

	schema := &models.ObjectMetadata{
		APIName:     "TestAccount",
		Label:       "Test Account Duplicate",
		PluralLabel: "Test Accounts Dup",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	err := ms.CreateSchema(ctx, schema)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_CreateSchema_InvalidAPIName(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	tests := []struct {
		name    string
		apiName string
	}{
		{"spaces", "bad name"},
		{"dashes", "bad-name"},
		{"starts with number", "1BadName"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			schema := &models.ObjectMetadata{
				APIName:     tt.apiName,
				Label:       "Test",
				PluralLabel: "Tests",
			}
			err := ms.CreateSchema(ctx, schema)
			assert.Error(t, err)
		})
	}
}

func TestMetadataService_CreateSchema_SchemaManagerError(t *testing.T) {
	ms, _, schemaRepo, _ := newTestService()
	ctx := context.Background()

	schemaRepo.createWithMetadataErr = fmt.Errorf("database connection lost")

	schema := &models.ObjectMetadata{
		APIName:     "TestObject",
		Label:       "Test",
		PluralLabel: "Tests",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	err := ms.CreateSchema(ctx, schema)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to create schema")
}

func TestMetadataService_UpdateSchema_HappyPath(t *testing.T) {
	ms, metaRepo, _, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.schemas["testaccount"] = &models.ObjectMetadata{
		ID:          "obj_testaccount",
		APIName:     "TestAccount",
		Label:       "Test Account",
		PluralLabel: "Test Accounts",
	}

	updates := &models.ObjectMetadata{
		Label:       "Updated Account",
		PluralLabel: "Updated Accounts",
		Icon:        "star",
	}

	err := ms.UpdateSchema(ctx, "TestAccount", updates)
	require.NoError(t, err)

	// Verify event published
	assert.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ObjectUpdated, eventPub.published[0].eventType)
}

func TestMetadataService_UpdateSchema_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.UpdateSchema(ctx, "NonExistent", &models.ObjectMetadata{Label: "New"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_DeleteSchema_HappyPath(t *testing.T) {
	ms, metaRepo, schemaRepo, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.schemas["testaccount"] = &models.ObjectMetadata{
		APIName: "TestAccount",
		Label:   "Test Account",
	}

	err := ms.DeleteSchema(ctx, "TestAccount")
	require.NoError(t, err)

	// Verify table was dropped
	assert.Contains(t, schemaRepo.droppedTables, "TestAccount")

	// Verify event published
	assert.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ObjectDeleted, eventPub.published[0].eventType)
}

func TestMetadataService_DeleteSchema_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.DeleteSchema(ctx, "NonExistent")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_DeleteSchema_DropTableError(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["testaccount"] = &models.ObjectMetadata{
		APIName: "TestAccount",
		Label:   "Test Account",
	}
	schemaRepo.dropTableErr = fmt.Errorf("permission denied")

	err := ms.DeleteSchema(ctx, "TestAccount")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to drop table")
}

// ============================================================================
// Tests: Schema Getters (Cache Behavior)
// ============================================================================

func TestMetadataService_GetSchema_CachePopulated(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Label:   "Account",
		Fields:  []models.FieldMetadata{{APIName: "Name", Type: constants.FieldTypeString}},
	}

	result := ms.GetSchema(ctx, "Account")
	require.NotNil(t, result)
	assert.Equal(t, "Account", result.APIName)
}

// Regression: GetSystemFields feeds the query-service visibleFields seed
// which becomes the raw SQL SELECT list. A virtual field marked IsSystem=true
// by the SchemaDecorator (e.g. Permissions<X> on PermissionSet) would
// otherwise land in SELECT and produce "Unknown column" against the physical
// table once stage 3 of the data-spec drops the bitmap columns. Filed
// against HEAD=be9dc7e0 by review; guarded here so the regression can't
// silently re-enter.
func TestMetadataService_GetSystemFields_ExcludesVirtualFields(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["permissionset"] = &models.ObjectMetadata{
		APIName: "PermissionSet",
		Label:   "Permission Set",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
			// Decorator-emitted virtual: IsSystem=true (matches SF describe
			// shape) AND IsVirtual=true (schema-declared virtual).
			{
				APIName:   "PermissionsViewSetup",
				Type:      constants.FieldTypeBoolean,
				IsSystem:  true,
				IsVirtual: true,
			},
		},
	}

	got := ms.GetSystemFields(ctx, "PermissionSet")
	// Id and Name must be present; the virtual must NOT be, because
	// GetSystemFields feeds a raw SQL SELECT list.
	assert.Contains(t, got, "Id")
	assert.Contains(t, got, "Name")
	assert.NotContains(t, got, "PermissionsViewSetup",
		"GetSystemFields must filter IsVirtual=true fields; they have no physical column and would break SQL SELECT")
}

func TestMetadataService_GetSchema_FallsBackToRepositoryOnCacheMiss(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	account := &models.ObjectMetadata{
		APIName: "Account",
		Label:   "Account",
	}
	file := &models.ObjectMetadata{
		APIName: "_System_File",
		Label:   "Files",
	}

	metaRepo.schemas["account"] = account
	metaRepo.schemas["_system_file"] = file
	ms.coreCache.Store(&metadataSnapshot{
		schemas:   []*models.ObjectMetadata{account},
		schemaMap: map[string]*models.ObjectMetadata{"account": account},
		fieldMap:  map[string][]models.FieldMetadata{"account": account.Fields},
	})

	result := ms.GetSchema(ctx, "_System_File")
	require.NotNil(t, result)
	assert.Equal(t, "_System_File", result.APIName)
	assert.Equal(t, "Files", result.Label)
}

func TestMetadataService_GetSchema_CaseInsensitive(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Label:   "Account",
	}

	// Different cases should all resolve to the same schema
	tests := []string{"Account", "account", "ACCOUNT", "aCcOuNt"}
	for _, name := range tests {
		t.Run(name, func(t *testing.T) {
			result := ms.GetSchema(ctx, name)
			require.NotNil(t, result)
			assert.Equal(t, "Account", result.APIName)
		})
	}
}

func TestMetadataService_DefaultRelatedListFields_UsesObjectListFields(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()

	quoteProduct := &models.ObjectMetadata{
		APIName:    "quote_product__c",
		Label:      "Quote Product",
		ListFields: []string{"Product__c", "Quantity__c", "TotalPrice__c"},
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Product__c", Type: constants.FieldTypeReference},
			{APIName: "Quantity__c", Type: constants.FieldTypeDouble},
			{APIName: "TotalPrice__c", Type: constants.FieldTypeCurrency},
		},
	}
	metaRepo.schemas["quote_product__c"] = quoteProduct
	ms.coreCache.Store(&metadataSnapshot{
		schemas: []*models.ObjectMetadata{quoteProduct},
		schemaMap: map[string]*models.ObjectMetadata{
			"quote_product__c": quoteProduct,
		},
		fieldMap: map[string][]models.FieldMetadata{
			"quote_product__c": quoteProduct.Fields,
		},
	})

	assert.Equal(t, []string{"Product__c", "Quantity__c", "TotalPrice__c"}, ms.defaultRelatedListFields("quote_product__c"))
}

func TestMetadataService_GetSchema_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetSchema(ctx, "NonExistent")
	assert.Nil(t, result)
}

func TestMetadataService_GetSchemaOrError_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result, err := ms.GetSchemaOrError(ctx, "NonExistent")
	assert.Nil(t, result)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_GetSchemaOrError_Found(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Label:   "Account",
	}

	result, err := ms.GetSchemaOrError(ctx, "Account")
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.Equal(t, "Account", result.APIName)
}

func TestMetadataService_GetSchemas_Empty(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetSchemas(ctx)
	assert.NotNil(t, result) // Should return empty slice, not nil
	assert.Empty(t, result)
}

func TestMetadataService_GetSchemas_WithData(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account", Label: "Account"}
	metaRepo.schemas["contact"] = &models.ObjectMetadata{APIName: "Contact", Label: "Contact"}

	result := ms.GetSchemas(ctx)
	assert.Len(t, result, 2)
}

func TestMetadataService_GetSearchableObjects(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account", Searchable: true, IsSystem: false}
	metaRepo.schemas["contact"] = &models.ObjectMetadata{APIName: "Contact", Searchable: true, IsSystem: false}
	metaRepo.schemas["_system_object"] = &models.ObjectMetadata{APIName: "_System_Object", Searchable: true, IsSystem: true}
	metaRepo.schemas["opportunity"] = &models.ObjectMetadata{APIName: "Opportunity", Searchable: false, IsSystem: false}

	result := ms.GetSearchableObjects(ctx)
	assert.Len(t, result, 2) // Only Account and Contact (non-system, searchable)
	for _, name := range result {
		assert.False(t, strings.HasPrefix(name, "_System_"))
	}
}

func TestMetadataService_GetSearchableObjects_ExcludesNonBusinessTableTypes(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName:    "Account",
		Searchable: true,
		TableType:  constants.TableTypeStandardObject,
	}
	metaRepo.schemas["custom"] = &models.ObjectMetadata{
		APIName:    "Invoice__c",
		Searchable: true,
		TableType:  constants.TableTypeCustomObject,
		IsCustom:   true,
	}
	metaRepo.schemas["tooling_custom_table_type"] = &models.ObjectMetadata{
		APIName:    "ApexClass",
		Searchable: true,
		TableType:  constants.TableTypeCustomObject,
		IsCustom:   false,
	}
	metaRepo.schemas["approval"] = &models.ObjectMetadata{
		APIName:    "_System_ApprovalProcess",
		Searchable: true,
		TableType:  constants.TableTypeSystemCore,
	}
	metaRepo.schemas["automation"] = &models.ObjectMetadata{
		APIName:    "_System_FlowDefinition",
		Searchable: true,
		TableType:  constants.TableType("system_automation"),
	}
	metaRepo.schemas["user_role"] = &models.ObjectMetadata{
		APIName:    constants.TableUserRole,
		Searchable: true,
		TableType:  constants.TableTypeStandardObject,
	}

	result := ms.GetSearchableObjects(ctx)
	assert.ElementsMatch(t, []string{"Account", "Invoice__c"}, result)
}

func TestMetadataService_GetField_Found(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString},
			{APIName: "Phone", Label: "Phone", Type: constants.FieldTypeString},
		},
	}

	// Prime cache
	_ = ms.GetSchema(ctx, "Account")

	field := ms.GetField("Account", "Name")
	require.NotNil(t, field)
	assert.Equal(t, "Name", field.APIName)
}

func TestMetadataService_GetField_NotFound(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString},
		},
	}

	_ = ms.GetSchema(ctx, "Account")

	field := ms.GetField("Account", "NonExistentField")
	assert.Nil(t, field)
}

// ============================================================================
// Tests: Cache Invalidation
// ============================================================================

func TestMetadataService_InvalidateCache(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account"}

	// Prime cache
	require.NoError(t, ms.RefreshCache(ctx))
	assert.NotNil(t, ms.loadCache())
	assert.NotNil(t, ms.loadCoreCache())

	// Invalidate
	ms.InvalidateCache()
	assert.Nil(t, ms.loadCache())
	assert.Nil(t, ms.loadCoreCache())
}

func TestMetadataService_RefreshCache_Error(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.getAllSchemasErr = fmt.Errorf("database unreachable")

	err := ms.RefreshCache(ctx)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to load schemas")
}

// ============================================================================
// Tests: Field CRUD
// ============================================================================

func TestMetadataService_CreateField_HappyPath(t *testing.T) {
	ms, metaRepo, schemaRepo, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Label:   "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	field := &models.FieldMetadata{
		APIName: "Industry",
		Label:   "Industry",
		Type:    constants.FieldTypeString,
	}

	err := ms.CreateField(ctx, "Account", field)
	require.NoError(t, err)

	// Verify column was added
	assert.Contains(t, schemaRepo.addedColumns, "Account")
	assert.Equal(t, "Industry", schemaRepo.addedColumns["Account"][0].Name)

	// Verify event published
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.FieldCreated, eventPub.published[0].eventType)
}

func TestMetadataService_CreateField_CanonicalizesObjectAPIName(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Label:   "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	field := &models.FieldMetadata{
		APIName: "Industry",
		Label:   "Industry",
		Type:    constants.FieldTypeString,
	}

	err := ms.CreateField(ctx, "account", field)
	require.NoError(t, err)
	assert.Contains(t, schemaRepo.addedColumns, "Account")
	assert.NotContains(t, schemaRepo.addedColumns, "account")
}

func TestMetadataService_CreateField_InvalidAPIName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields:  []models.FieldMetadata{},
	}

	tests := []struct {
		name    string
		apiName string
	}{
		{"spaces", "bad field"},
		{"dashes", "bad-field"},
		{"starts with number", "1Field"},
		{"special chars", "field@name"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			field := &models.FieldMetadata{
				APIName: tt.apiName,
				Label:   "Test",
				Type:    constants.FieldTypeString,
			}
			err := ms.CreateField(ctx, "Account", field)
			assert.Error(t, err)
		})
	}
}

func TestMetadataService_CreateField_SystemFieldRestriction(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields:  []models.FieldMetadata{},
	}

	field := &models.FieldMetadata{
		APIName: "CreatedDate",
		Label:   "Created Date",
		Type:    constants.FieldTypeDatetime,
	}

	err := ms.CreateField(ctx, "Account", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "reserved system name")
}

func TestMetadataService_CreateField_DuplicateField(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Industry", Label: "Industry", Type: constants.FieldTypeString},
		},
	}

	field := &models.FieldMetadata{
		APIName: "Industry",
		Label:   "Industry Again",
		Type:    constants.FieldTypeString,
	}

	err := ms.CreateField(ctx, "Account", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_CreateField_ObjectNotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	field := &models.FieldMetadata{
		APIName: "TestField",
		Label:   "Test",
		Type:    constants.FieldTypeString,
	}

	err := ms.CreateField(ctx, "NonExistent", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_CreateField_PicklistRequiresOptions(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields:  []models.FieldMetadata{},
	}

	field := &models.FieldMetadata{
		APIName: "Status",
		Label:   "Status",
		Type:    constants.FieldTypePicklist,
		Options: models.PicklistOptionsFromStrings([]string{}), // Empty
	}

	err := ms.CreateField(ctx, "Account", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "Picklist fields require at least one option")
}

func TestMetadataService_CreateField_PicklistWithOptions(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	field := &models.FieldMetadata{
		APIName: "Status",
		Label:   "Status",
		Type:    constants.FieldTypePicklist,
		Options: models.PicklistOptionsFromStrings([]string{"Open", "Closed", "Pending"}),
	}

	err := ms.CreateField(ctx, "Account", field)
	require.NoError(t, err)
}

func TestMetadataService_CreateField_LookupRequiresReferenceTo(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:      "obj_contact",
		APIName: "Contact",
		Fields:  []models.FieldMetadata{},
	}

	field := &models.FieldMetadata{
		APIName:     "AccountId",
		Label:       "Account",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{}, // Empty
	}

	err := ms.CreateField(ctx, "Contact", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "Lookup fields require a referenced object")
}

func TestMetadataService_CreateField_LookupWithReferenceTo(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:      "obj_contact",
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	field := &models.FieldMetadata{
		APIName:     "AccountId",
		Label:       "Account",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Account"},
	}

	err := ms.CreateField(ctx, "Contact", field)
	require.NoError(t, err)
}

func TestMetadataService_CreateField_MasterDetailEnforcements(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:      "obj_contact",
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	field := &models.FieldMetadata{
		APIName:        "AccountId",
		Label:          "Account",
		Type:           constants.FieldTypeReference,
		ReferenceTo:    []string{"Account"},
		IsMasterDetail: true,
	}

	err := ms.CreateField(ctx, "Contact", field)
	require.NoError(t, err)

	// Verify enforced defaults
	assert.True(t, field.Required, "Master-Detail should enforce Required=true")
	assert.NotNil(t, field.DeleteRule, "Master-Detail should set DeleteRule")
	assert.Equal(t, constants.DeleteRuleCascade, *field.DeleteRule)
}

func TestMetadataService_CreateField_MasterDetailWrongType(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:      "obj_contact",
		APIName: "Contact",
		Fields:  []models.FieldMetadata{},
	}

	field := &models.FieldMetadata{
		APIName:        "BadField",
		Label:          "Bad",
		Type:           constants.FieldTypeString, // Not Reference!
		IsMasterDetail: true,
	}

	err := ms.CreateField(ctx, "Contact", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "Master-Detail relationship is only valid for Lookup")
}

func TestMetadataService_CreateField_AddColumnError(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	schemaRepo.addColumnErr = fmt.Errorf("column already exists")

	field := &models.FieldMetadata{
		APIName: "Revenue",
		Label:   "Revenue",
		Type:    constants.FieldTypeCurrency,
	}

	err := ms.CreateField(ctx, "Account", field)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to add column")
}

func TestMetadataService_UpdateField_HappyPath(t *testing.T) {
	ms, metaRepo, _, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Industry", Label: "Industry", Type: constants.FieldTypeString},
		},
	}

	updates := &models.FieldMetadata{
		Label:    "Industry Sector",
		Required: true,
	}

	err := ms.UpdateField(ctx, "Account", "Industry", updates)
	require.NoError(t, err)

	// Verify event
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.FieldUpdated, eventPub.published[0].eventType)
}

func TestMetadataService_UpdateField_ObjectNotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.UpdateField(ctx, "NonExistent", "Field1", &models.FieldMetadata{Label: "New"})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_UpdateField_FieldNotFound(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString},
		},
	}

	err := ms.UpdateField(ctx, "Account", "NonExistentField", &models.FieldMetadata{Label: "New"})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

// TestMetadataService_UpdateField_RejectsSemicolonInMultiPicklistOption pins
// the UpdateField path for #1686. The validator existed only on CreateField /
// field_batch before this change; UpdateField would silently accept a
// malformed option value and persist it, re-opening the #1683 self-update
// loop for records that later land on a flow-driven field.
func TestMetadataService_UpdateField_RejectsSemicolonInMultiPicklistOption(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	// newTestService omits validationSvc for tests that don't exercise it;
	// wire it explicitly here since this test is specifically pinning the
	// validator's enforcement on the UpdateField path.
	ms.SetValidationService(NewValidationService(formula.NewEngine()))
	ctx := context.Background()

	metaRepo.schemas["opportunity"] = &models.ObjectMetadata{
		ID:      "obj_opportunity",
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{
				APIName: "Tags__c", Label: "Tags", Type: constants.FieldTypeMultiPicklist,
				Options: models.PicklistOptions{{Value: "Alpha", Label: "Alpha"}},
			},
		},
	}

	updates := &models.FieldMetadata{
		Options: models.PicklistOptions{
			{Value: "Alpha", Label: "Alpha"},
			{Value: "Needs Review; Urgent", Label: "Needs Review; Urgent"},
		},
	}

	err := ms.UpdateField(ctx, "Opportunity", "Tags__c", updates)
	require.Error(t, err)
	assert.Contains(t, err.Error(), ";")
}

func TestMetadataService_UpdateField_SystemFieldRestrictions(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "CreatedDate", Label: "Created Date", Type: constants.FieldTypeDatetime, IsSystem: true},
		},
	}

	// Trying to change type of a system field
	updates := &models.FieldMetadata{
		Type: constants.FieldTypeString, // Change type
	}

	err := ms.UpdateField(ctx, "Account", "CreatedDate", updates)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "cannot modify structural properties")
}

func TestMetadataService_DeleteField_HappyPath(t *testing.T) {
	ms, metaRepo, schemaRepo, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Industry", Label: "Industry", Type: constants.FieldTypeString},
		},
	}

	err := ms.DeleteField(ctx, "Account", "Industry")
	require.NoError(t, err)

	// Verify column was dropped
	assert.Contains(t, schemaRepo.droppedColumns, "Account")
	assert.Contains(t, schemaRepo.droppedColumns["Account"], "Industry")

	// Verify event
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.FieldDeleted, eventPub.published[0].eventType)
}

func TestMetadataService_DeleteField_ObjectNotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.DeleteField(ctx, "NonExistent", "Field1")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_DeleteField_FieldNotFound(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString},
		},
	}

	err := ms.DeleteField(ctx, "Account", "NonExistent")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_DeleteField_SystemFieldRestriction(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	t.Run("cannot delete system field", func(t *testing.T) {
		err := ms.DeleteField(ctx, "Account", "Id")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "cannot delete system")
	})

	t.Run("cannot delete name field", func(t *testing.T) {
		err := ms.DeleteField(ctx, "Account", "Name")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "cannot delete system or name field")
	})
}

// ============================================================================
// Tests: Layout Operations
// ============================================================================

func TestMetadataService_GetLayout_FromRepo(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	// Populate schemas for cache
	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Fields:  []models.FieldMetadata{{APIName: "Name", Type: constants.FieldTypeString}},
	}

	// Populate layout - use constants.LayoutNameDefault ("Default Layout") not hardcoded "Default"
	layout := &models.PageLayout{
		ID:            "layout_1",
		ObjectAPIName: "Account",
		LayoutName:    constants.LayoutNameDefault,
		Type:          "Detail",
		LayoutVersion: currentLayoutVersion,
		Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
	}
	metaRepo.layouts["account"] = []*models.PageLayout{layout}

	result := ms.GetLayout(ctx, "Account", nil)
	require.NotNil(t, result)
	assert.Equal(t, "layout_1", result.ID)
	assert.Equal(t, constants.LayoutNameDefault, result.LayoutName)
}

func TestMetadataService_GetLayout_FallbackToGenerated(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "CreatedDate", Type: constants.FieldTypeDatetime, IsSystem: true},
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Industry", Label: "Industry", Type: constants.FieldTypeString},
		},
	}
	// No layouts in repo -> should generate default

	result := ms.GetLayout(ctx, "Account", nil)
	require.NotNil(t, result)
	assert.Equal(t, "default_Account", result.ID)
	assert.Equal(t, constants.LayoutNameDefault, result.LayoutName)

	// Invisible fields (Id) should NOT be in any section
	for _, section := range result.Sections {
		for _, f := range section.Fields {
			assert.NotEqual(t, "Id", f, "Invisible field Id should not be in generated layout")
		}
	}

	// Business fields should be present in the first section
	assert.Contains(t, result.Sections[0].Fields, "Name")
	assert.Contains(t, result.Sections[0].Fields, "Industry")

	// System audit fields (CreatedDate) should appear in a "System Information" section
	// (matching Salesforce page layout behavior)
	var sysSection *models.PageSection
	for i := range result.Sections {
		if result.Sections[i].Label == constants.SectionLabelSystemInfo {
			sysSection = &result.Sections[i]
			break
		}
	}
	require.NotNil(t, sysSection, "Should have a System Information section")
	assert.Contains(t, sysSection.Fields, "CreatedDate")
}

func TestMetadataService_ResolveLayout_UsesPersistedLayoutWhenAssignmentMissing(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.disableImplicitLayoutAssignment = true
	metaRepo.schemas["task"] = &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{
			{APIName: "Subject", Label: "Subject", Type: constants.FieldTypeString},
		},
	}

	layout := &models.PageLayout{
		ID:            "layout_task_real",
		ObjectAPIName: "Task",
		LayoutName:    constants.LayoutNameDefault,
		Type:          "Detail",
		IsDefault:     true,
		Source:        "admin_created",
		Sections:      []models.PageSection{{Label: "Task Information", Fields: []string{"Subject"}}},
	}
	metaRepo.storeLayout(layout)

	result := ms.ResolveLayout(ctx, "Task", nil, "", "")
	require.NotNil(t, result)
	assert.Equal(t, layout.ID, result.ID)

	assignments := metaRepo.assignmentsByObject["task"]
	require.Len(t, assignments, 1)
	assert.Equal(t, layout.ID, assignments[0].LayoutID)
	assert.Zero(t, assignments[0].Specificity)
	require.NotNil(t, assignments[0].RouteSource)
	assert.Equal(t, models.LayoutAssignmentRouteSourceRuntimeFallback, *assignments[0].RouteSource)
	assert.Empty(t, metaRepo.deletedAssignmentIDs)
}

func TestMetadataService_ResolveLayout_RegeneratesPersistedAutoLayoutInPlace(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.disableImplicitLayoutAssignment = true
	metaRepo.schemas["task"] = &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "CreatedDate", Label: "Created Date", Type: constants.FieldTypeDatetime, IsSystem: true},
			{APIName: "Subject", Label: "Subject", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	layout := &models.PageLayout{
		ID:                "layout_task_real",
		ObjectAPIName:     "Task",
		LayoutName:        constants.LayoutNameDefault,
		Type:              "Detail",
		IsDefault:         true,
		Source:            "auto_generated",
		GenerationVersion: currentGenerationVersion - 1,
		LayoutVersion:     currentGenerationVersion - 1,
		Sections:          []models.PageSection{{Label: "Old Section", Fields: []string{"Subject"}}},
	}
	metaRepo.storeLayout(layout)

	result := ms.ResolveLayout(ctx, "Task", nil, "", "")
	require.NotNil(t, result)
	assert.Equal(t, layout.ID, result.ID)
	assert.Equal(t, currentGenerationVersion, result.GenerationVersion)
	assert.Equal(t, currentGenerationVersion, metaRepo.layoutByID[layout.ID].GenerationVersion)

	assignments := metaRepo.assignmentsByObject["task"]
	require.Len(t, assignments, 1)
	assert.Equal(t, layout.ID, assignments[0].LayoutID)
	require.NotNil(t, assignments[0].RouteSource)
	assert.Equal(t, models.LayoutAssignmentRouteSourceRuntimeFallback, *assignments[0].RouteSource)
}

func TestMetadataService_ResolveLayout_RepairsOrphanedWildcardAssignmentToRealLayout(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.disableImplicitLayoutAssignment = true
	metaRepo.schemas["task"] = &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{
			{APIName: "Subject", Label: "Subject", Type: constants.FieldTypeString},
		},
	}

	layout := &models.PageLayout{
		ID:            "layout_task_real",
		ObjectAPIName: "Task",
		LayoutName:    constants.LayoutNameDefault,
		Type:          "Detail",
		IsDefault:     true,
		Source:        "admin_created",
		Sections:      []models.PageSection{{Label: "Task Information", Fields: []string{"Subject"}}},
	}
	metaRepo.storeLayout(layout)
	metaRepo.assignmentsByObject["task"] = []*models.LayoutAssignment{
		{
			ID:            "assignment_orphan",
			ObjectAPIName: "Task",
			LayoutID:      "default_Task",
			Specificity:   0,
		},
	}

	result := ms.ResolveLayout(ctx, "Task", nil, "", "")
	require.NotNil(t, result)
	assert.Equal(t, layout.ID, result.ID)
	assert.Equal(t, []string{"assignment_orphan"}, metaRepo.deletedAssignmentIDs)

	assignments := metaRepo.assignmentsByObject["task"]
	require.Len(t, assignments, 1)
	assert.Equal(t, layout.ID, assignments[0].LayoutID)
	assert.Zero(t, assignments[0].Specificity)
	require.NotNil(t, assignments[0].RouteSource)
	assert.Equal(t, models.LayoutAssignmentRouteSourceRuntimeFallback, *assignments[0].RouteSource)
}

func TestMetadataService_GetLayout_ObjectNotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetLayout(ctx, "NonExistent", nil)
	assert.Nil(t, result)
}

func TestMetadataService_SaveLayout(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	layout := &models.PageLayout{
		ID:            "layout_1",
		ObjectAPIName: "Account",
		LayoutName:    "Custom Layout",
	}

	err := ms.SaveLayout(ctx, layout)
	require.NoError(t, err)
	assert.Contains(t, metaRepo.layoutByID, "layout_1")
}

func TestMetadataService_SaveLayout_Error(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.saveLayoutErr = fmt.Errorf("database error")

	err := ms.SaveLayout(ctx, &models.PageLayout{ID: "layout_1"})
	assert.Error(t, err)
}

func TestMetadataService_DeleteLayout(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.layoutByID["layout_1"] = &models.PageLayout{ID: "layout_1"}

	err := ms.DeleteLayout(ctx, "layout_1")
	require.NoError(t, err)
	assert.NotContains(t, metaRepo.layoutByID, "layout_1")
}

func TestMetadataService_AssignLayoutToProfile(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	err := ms.AssignLayoutToProfile(ctx, "profile_admin", "Account", "layout_1")
	require.NoError(t, err)
	assert.Equal(t, "layout_1", metaRepo.profileLayouts["profile_adminAccount"])
}

func TestMetadataService_AssignLayoutToProfile_CanonicalizesObjectAPIName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
	}

	err := ms.AssignLayoutToProfile(ctx, "profile_admin", "account", "layout_1")
	require.NoError(t, err)
	assert.Equal(t, "layout_1", metaRepo.profileLayouts["profile_adminAccount"])
}

// ============================================================================
// Tests: Action CRUD
// ============================================================================

func TestMetadataService_CreateAction_HappyPath(t *testing.T) {
	ms, metaRepo, _, eventPub := newTestService()
	ctx := context.Background()

	action := &models.ActionMetadata{
		Name:          "Send_Email",
		Label:         "Send Email",
		Type:          "QuickAction",
		ObjectAPIName: "Account",
	}

	err := ms.CreateAction(ctx, action)
	require.NoError(t, err)

	// Verify ID was generated
	assert.NotEmpty(t, action.ID)

	// Verify stored
	assert.Contains(t, metaRepo.actions, action.ID)

	// Verify event
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ActionCreated, eventPub.published[0].eventType)
}

// TestMetadataService_CreateAction_RejectsRetiredChildCopySurface is
// the Phase 2b authoring-surface closure. workflow.childCopy was
// retired in Phase 2 (the action-handler executor is deleted); any
// admin authoring that tries to land new workflow.childCopy entries
// must be rejected, otherwise the surface becomes a fake metadata
// surface (writers accept / runtime ignores).
//
// The rejection applies to CLEAN entries as well as entries carrying
// legacy sub-keys (sourceIdField / hydrateSource) — the whole subtree
// is dead, not just the sub-keys.
func TestMetadataService_CreateAction_RejectsRetiredChildCopySurface(t *testing.T) {
	cleanEntry := map[string]interface{}{
		"sourceObject":      "OpportunityLineItem",
		"sourceLookupField": "OpportunityId",
		"targetObject":      "quote_product__c",
		"targetLookupField": "Quote__c",
	}
	entryWithSourceIdField := map[string]interface{}{
		"sourceIdField":     "Quote_Product__c",
		"sourceObject":      "OpportunityLineItem",
		"sourceLookupField": "OpportunityId",
		"targetObject":      "quote_product__c",
		"targetLookupField": "Quote__c",
	}
	cases := []struct {
		name  string
		entry map[string]interface{}
	}{
		{"clean entry (no sub-keys)", cleanEntry},
		{"entry with sourceIdField", entryWithSourceIdField},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ms, _, _, _ := newTestService()
			ctx := context.Background()

			action := &models.ActionMetadata{
				Name:          "Admin_Authored_Retired_Action",
				Label:         "Admin Authored",
				Type:          "CreateRecord",
				ObjectAPIName: "Opportunity",
				Config: map[string]interface{}{
					"workflow": map[string]interface{}{
						"childCopy": []interface{}{tc.entry},
					},
				},
			}

			err := ms.CreateAction(ctx, action)
			require.Error(t, err, "CreateAction must reject any non-empty workflow.childCopy")
			assert.Contains(t, err.Error(), "retired",
				"error must name the surface as retired")
			assert.Contains(t, err.Error(), "_System_Object.ChildCloneRules",
				"error must point admin at the correct authoring site")
		})
	}
}

// TestMetadataService_UpdateAction_RejectsRetiredChildCopySurface
// mirrors the CreateAction closure for Update — admin patches that
// re-author workflow.childCopy entries are rejected, even if the
// entries are shape-clean. The update is an authoring event; the
// surface is retired; the write fails.
func TestMetadataService_UpdateAction_RejectsRetiredChildCopySurface(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	existing := &models.ActionMetadata{
		ID:            "action-strict-1",
		Name:          "Admin_Update_Action",
		Label:         "L",
		Type:          "CreateRecord",
		ObjectAPIName: "Opportunity",
	}
	metaRepo.actions[existing.ID] = existing

	updates := &models.ActionMetadata{
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"childCopy": []interface{}{
					map[string]interface{}{
						"sourceObject":      "OpportunityLineItem",
						"sourceLookupField": "OpportunityId",
						"targetObject":      "quote_product__c",
						"targetLookupField": "Quote__c",
					},
				},
			},
		},
	}

	err := ms.UpdateAction(ctx, existing.ID, updates)
	require.Error(t, err, "UpdateAction must reject re-authoring of retired workflow.childCopy")
	assert.Contains(t, err.Error(), "retired")
}

// TestMetadataService_UpdateAction_EmptyChildCopyPasses proves the
// operator lever: an update that submits workflow.childCopy = [] is
// the clear-intent signal. The primitive passes it so the merged
// Config can take effect (deep-merge replaces arrays wholesale, so
// this wipes residue if any existed in the stored Config).
func TestMetadataService_UpdateAction_EmptyChildCopyPasses(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	existing := &models.ActionMetadata{
		ID:            "action-clear-childcopy",
		Name:          "Admin_Clear_Action",
		Label:         "L",
		Type:          "CreateRecord",
		ObjectAPIName: "Opportunity",
	}
	metaRepo.actions[existing.ID] = existing

	updates := &models.ActionMetadata{
		Config: map[string]interface{}{
			"workflow": map[string]interface{}{
				"childCopy": []interface{}{},
			},
		},
	}

	err := ms.UpdateAction(ctx, existing.ID, updates)
	require.NoError(t, err, "empty childCopy must be an accepted clear signal")
}

func TestMetadataService_CreateAction_MissingFields(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	tests := []struct {
		name   string
		action *models.ActionMetadata
		errMsg string
	}{
		{
			name:   "missing name",
			action: &models.ActionMetadata{Label: "Test", Type: "QuickAction"},
			errMsg: "action name is required",
		},
		{
			name:   "missing label",
			action: &models.ActionMetadata{Name: "Test", Type: "QuickAction"},
			errMsg: "action label is required",
		},
		{
			name:   "missing type",
			action: &models.ActionMetadata{Name: "Test", Label: "Test"},
			errMsg: "action type is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ms.CreateAction(ctx, tt.action)
			require.Error(t, err)
			assert.Contains(t, err.Error(), tt.errMsg)
		})
	}
}

func TestMetadataService_CreateAction_DuplicateName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	// Pre-existing action
	metaRepo.actions["existing_id"] = &models.ActionMetadata{
		ID:            "existing_id",
		Name:          "Send_Email",
		Label:         "Send Email",
		Type:          "QuickAction",
		ObjectAPIName: "Account",
	}

	newAction := &models.ActionMetadata{
		Name:          "Send_Email",
		Label:         "Send Email Again",
		Type:          "QuickAction",
		ObjectAPIName: "Account",
	}

	err := ms.CreateAction(ctx, newAction)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_UpdateAction_HappyPath(t *testing.T) {
	ms, metaRepo, _, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.actions["action_1"] = &models.ActionMetadata{
		ID:            "action_1",
		Name:          "Send_Email",
		Label:         "Send Email",
		Type:          "QuickAction",
		ObjectAPIName: "Account",
	}

	updates := &models.ActionMetadata{
		Label: "Send Email V2",
	}

	err := ms.UpdateAction(ctx, "action_1", updates)
	require.NoError(t, err)

	// Verify event
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ActionUpdated, eventPub.published[0].eventType)
}

func TestMetadataService_UpdateAction_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.UpdateAction(ctx, "nonexistent", &models.ActionMetadata{Label: "New"})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_DeleteAction_HappyPath(t *testing.T) {
	ms, metaRepo, _, eventPub := newTestService()
	ctx := context.Background()

	metaRepo.actions["action_1"] = &models.ActionMetadata{
		ID:   "action_1",
		Name: "Send_Email",
	}

	err := ms.DeleteAction(ctx, "action_1")
	require.NoError(t, err)
	assert.NotContains(t, metaRepo.actions, "action_1")

	// Verify event
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ActionDeleted, eventPub.published[0].eventType)
}

func TestMetadataService_DeleteAction_Error(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.deleteActionErr = fmt.Errorf("database locked")

	err := ms.DeleteAction(ctx, "action_1")
	assert.Error(t, err)
}

// ============================================================================
// Tests: Flow Definition CRUD
// ============================================================================

func TestMetadataService_CreateFlowDefinition_HappyPath(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	def := &models.FlowDefinition{
		MasterLabel:   "Test Flow",
		DeveloperName: "Test_Flow",
		TriggerType:   "RecordAfterSave",
	}

	err := ms.CreateFlowDefinition(ctx, def)
	require.NoError(t, err)
	assert.NotEmpty(t, def.Id)
	assert.Contains(t, metaRepo.flowDefinitions, def.Id)
	assertMarkedFlowDefinitions(t, metaRepo, def.Id)
}

func TestMetadataService_GetFlowDefinitions_ReturnsCachedRows(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	rows := []*models.FlowDefinition{
		{Id: "fd_user", DeveloperName: "User_Flow", ProcessType: "AutoLaunchedFlow"},
		{Id: "fd_platform", DeveloperName: "Platform_Flow", ProcessType: "AutoLaunchedFlow"},
		{Id: "fd_tenant", DeveloperName: "Tenant_Flow", ProcessType: "AutoLaunchedFlow"},
	}
	for _, r := range rows {
		metaRepo.flowDefinitions[r.Id] = r
	}

	all := ms.GetFlowDefinitions(ctx)
	byID := make(map[string]string)
	for _, d := range all {
		byID[d.Id] = d.DeveloperName
	}
	assert.Equal(t, "User_Flow", byID["fd_user"])
	assert.Equal(t, "Platform_Flow", byID["fd_platform"])
	assert.Equal(t, "Tenant_Flow", byID["fd_tenant"])

	got := ms.GetFlowDefinitionByName(ctx, "Tenant_Flow")
	require.NotNil(t, got)
	assert.Equal(t, "fd_tenant", got.Id)

	gotByID := ms.GetFlowDefinition(ctx, "fd_platform")
	require.NotNil(t, gotByID)
	assert.Equal(t, "Platform_Flow", gotByID.DeveloperName)
}

func TestMetadataService_CreateFlowDefinition_Error(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.createFlowErr = fmt.Errorf("unique constraint violation")

	err := ms.CreateFlowDefinition(ctx, &models.FlowDefinition{})
	assert.Error(t, err)
}

func TestMetadataService_UpdateFlowDefinition(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()
	updated := "Updated"

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:          "flow_1",
		MasterLabel: "Original",
	}

	err := ms.UpdateFlowDefinition(ctx, "flow_1", &models.FlowDefinitionPatch{MasterLabel: &updated})
	require.NoError(t, err)
	require.Len(t, metaRepo.patchFlowDefinitionCalls, 1)
	require.NotNil(t, metaRepo.patchFlowDefinitionCalls[0].patch.MasterLabel)
	assert.Equal(t, "Updated", *metaRepo.patchFlowDefinitionCalls[0].patch.MasterLabel)
	assert.Equal(t, "Updated", metaRepo.flowDefinitions["flow_1"].MasterLabel)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_1")
}

func TestMetadataService_UpdateFlowDefinition_PreservesTombstone(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()
	updated := "Updated"

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:          "flow_1",
		MasterLabel: "Original",
		IsDeleted:   true,
	}

	err := ms.UpdateFlowDefinition(ctx, "flow_1", &models.FlowDefinitionPatch{MasterLabel: &updated})
	require.NoError(t, err)
	require.True(t, metaRepo.flowDefinitions["flow_1"].IsDeleted)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_1")
}

func TestMetadataService_DeleteFlowDefinition(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{Id: "flow_1"}

	err := ms.DeleteFlowDefinition(ctx, "flow_1")
	require.NoError(t, err)
	assert.NotContains(t, metaRepo.flowDefinitions, "flow_1")
}

func TestMetadataService_GetFlowDefinitions_FromCache(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:          "flow_1",
		MasterLabel: "Flow 1",
	}
	metaRepo.flowDefinitions["flow_2"] = &models.FlowDefinition{
		Id:          "flow_2",
		MasterLabel: "Flow 2",
	}

	flows := ms.GetFlowDefinitions(ctx)
	assert.Len(t, flows, 2)
}

func TestMetadataService_GetFlowDefinition_ByID(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:          "flow_1",
		MasterLabel: "Flow 1",
	}

	result := ms.GetFlowDefinition(ctx, "flow_1")
	require.NotNil(t, result)
	assert.Equal(t, "Flow 1", result.MasterLabel)
}

func TestMetadataService_GetFlowDefinition_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetFlowDefinition(ctx, "nonexistent")
	assert.Nil(t, result)
}

func TestMetadataService_GetFlowDefinitionByName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:            "flow_1",
		MasterLabel:   "My Flow",
		DeveloperName: "My_Flow",
	}

	result := ms.GetFlowDefinitionByName(ctx, "My_Flow")
	require.NotNil(t, result)
	assert.Equal(t, "flow_1", result.Id)
}

func TestMetadataService_GetScheduledFlowDefinitions(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:          "flow_1",
		TriggerType: "Scheduled",
	}
	metaRepo.flowDefinitions["flow_2"] = &models.FlowDefinition{
		Id:          "flow_2",
		TriggerType: "RecordAfterSave",
	}
	metaRepo.flowDefinitions["flow_3"] = &models.FlowDefinition{
		Id:          "flow_3",
		TriggerType: "Scheduled",
	}

	scheduled := ms.GetScheduledFlowDefinitions(ctx)
	assert.Len(t, scheduled, 2)
	for _, s := range scheduled {
		assert.Equal(t, "Scheduled", s.TriggerType)
	}
}

// ============================================================================
// Tests: Flow Version CRUD
// ============================================================================

func TestMetadataService_CreateFlowVersion_HappyPath(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{Id: "flow_def_1"}

	version := &models.FlowVersion{
		DefinitionId:  "flow_def_1",
		VersionNumber: 1,
	}

	err := ms.CreateFlowVersion(ctx, version)
	require.NoError(t, err)
	assert.NotEmpty(t, version.Id)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_def_1")
	require.NotNil(t, metaRepo.flowVersions[version.Id])
	assert.Equal(t, flowSourceCustom, metaRepo.flowVersions[version.Id].Source)
}

// TestMetadataService_HasActiveRecordTriggeredFlow_Matches asserts the
// happy path used by OutboxService: a FlowDefinition with the right
// TriggerType + non-empty ActiveVersionId + matching object name (case
// insensitive) makes the lookup return true. Mismatch on any of those
// three returns false.
func TestMetadataService_HasActiveRecordTriggeredFlow_Matches(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["fd-order"] = &models.FlowDefinition{
		Id:                   "fd-order",
		TriggerType:          "RecordAfterSave",
		TriggerObjectOrEvent: "Order",
		ActiveVersionId:      "v-1",
	}
	metaRepo.flowDefinitions["fd-inactive"] = &models.FlowDefinition{
		Id:                   "fd-inactive",
		TriggerType:          "RecordAfterSave",
		TriggerObjectOrEvent: "Lead",
		ActiveVersionId:      "", // draft, not active — must not match
	}
	metaRepo.flowDefinitions["fd-scheduled"] = &models.FlowDefinition{
		Id:                   "fd-scheduled",
		TriggerType:          "Scheduled", // not a record trigger — must not match
		TriggerObjectOrEvent: "Account",
		ActiveVersionId:      "v-2",
	}

	t.Run("matches active record trigger case-insensitive", func(t *testing.T) {
		assert.True(t, ms.HasActiveRecordTriggeredFlow(ctx, "Order"))
		assert.True(t, ms.HasActiveRecordTriggeredFlow(ctx, "order"))
		assert.True(t, ms.HasActiveRecordTriggeredFlow(ctx, "ORDER"))
	})
	t.Run("ignores inactive (empty ActiveVersionId)", func(t *testing.T) {
		assert.False(t, ms.HasActiveRecordTriggeredFlow(ctx, "Lead"))
	})
	t.Run("ignores non-record trigger types", func(t *testing.T) {
		assert.False(t, ms.HasActiveRecordTriggeredFlow(ctx, "Account"))
	})
	t.Run("returns false for unknown object", func(t *testing.T) {
		assert.False(t, ms.HasActiveRecordTriggeredFlow(ctx, "Contact"))
	})
}

// TestIsRecordTriggerType_CaseInsensitive pins the EqualFold fix for
// isRecordTriggerType so it stays consistent with the listener's
// EqualFold match on TriggerType. Flow definitions created via external
// tooling may store TriggerType with non-canonical casing.
func TestIsRecordTriggerType_CaseInsensitive(t *testing.T) {
	tests := []struct {
		input string
		want  bool
	}{
		// canonical PascalCase — always true
		{"RecordBeforeSave", true},
		{"RecordAfterSave", true},
		{"RecordBeforeDelete", true},
		{"RecordAfterDelete", true},
		// non-canonical casing — must still be true after EqualFold fix
		{"recordaftersave", true},
		{"recordAfterSave", true},
		{"RECORDAFTERSAVE", true},
		{"recordbeforesave", true},
		// non-record trigger types — always false
		{"Scheduled", false},
		{"AutoLaunchedFlow", false},
		{"", false},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			assert.Equal(t, tt.want, isRecordTriggerType(tt.input))
		})
	}
}

// TestHasActiveRecordTriggeredFlow_MixedCaseTriggerType verifies that
// HasActiveRecordTriggeredFlow correctly identifies a flow with non-canonical
// TriggerType casing as an active record-triggered flow.
func TestHasActiveRecordTriggeredFlow_MixedCaseTriggerType(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	// Simulate a flow stored with lowercase TriggerType (defensive guard)
	metaRepo.flowDefinitions["fd-mixed"] = &models.FlowDefinition{
		Id:                   "fd-mixed",
		TriggerType:          "recordAfterSave", // non-canonical casing
		TriggerObjectOrEvent: "Contact",
		ActiveVersionId:      "v-1",
	}

	assert.True(t, ms.HasActiveRecordTriggeredFlow(ctx, "Contact"),
		"mixed-case TriggerType must still be recognised as record trigger")
	assert.False(t, ms.HasActiveRecordTriggeredFlow(ctx, "Lead"),
		"unknown object must return false")
}

func TestMetadataService_GetFlowVersionById(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:            "v_1",
		DefinitionId:  "flow_1",
		VersionNumber: 1,
	}

	result := ms.GetFlowVersionById(ctx, "v_1")
	require.NotNil(t, result)
	assert.Equal(t, "v_1", result.Id)
}

func TestMetadataService_GetFlowVersionById_Empty(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetFlowVersionById(ctx, "")
	assert.Nil(t, result)
}

func TestMetadataService_GetFlowVersions(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	t.Run("empty definition id returns empty", func(t *testing.T) {
		versions, err := ms.GetFlowVersions(ctx, "")
		require.NoError(t, err)
		assert.Empty(t, versions)
	})
}

func TestMetadataService_ActivateFlowVersion_CustomSource_MarksCustomized(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{Id: "flow_def_1"}
	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:           "v_1",
		DefinitionId: "flow_def_1",
		Source:       flowSourceCustom,
	}

	err := ms.ActivateFlowVersion(ctx, "v_1")
	require.NoError(t, err)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_def_1")
	assert.True(t, metaRepo.flowDefinitions["flow_def_1"].IsCustomized)
}

func TestMetadataService_ActivateFlowVersion_StandardSource_DoesNotMarkCustomized(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{Id: "flow_def_1"}
	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:           "v_1",
		DefinitionId: "flow_def_1",
		Source:       "standard",
	}

	err := ms.ActivateFlowVersion(ctx, "v_1")
	require.NoError(t, err)
	assertMarkedFlowDefinitions(t, metaRepo)
	assert.False(t, metaRepo.flowDefinitions["flow_def_1"].IsCustomized)
}

func TestMetadataService_ActivateFlowVersion_StandardSource_DoesNotResetCustomized(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{
		Id:           "flow_def_1",
		IsCustomized: true,
	}
	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:           "v_1",
		DefinitionId: "flow_def_1",
		Source:       "standard",
	}

	err := ms.ActivateFlowVersion(ctx, "v_1")
	require.NoError(t, err)
	assertMarkedFlowDefinitions(t, metaRepo)
	assert.True(t, metaRepo.flowDefinitions["flow_def_1"].IsCustomized)
}

func TestMetadataService_DeactivateFlowVersion_MarksCustomized(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{Id: "flow_def_1"}
	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:           "v_1",
		DefinitionId: "flow_def_1",
	}

	err := ms.DeactivateFlowVersion(ctx, "v_1")
	require.NoError(t, err)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_def_1")
	assert.True(t, metaRepo.flowDefinitions["flow_def_1"].IsCustomized)
}

func TestMetadataService_UpdateFlowVersion_UsesStoredDefinitionIdWhenPayloadOmitsIt(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{Id: "flow_def_1"}
	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:           "v_1",
		DefinitionId: "flow_def_1",
		Source:       flowSourceCustom,
	}

	err := ms.UpdateFlowVersion(ctx, "v_1", &models.FlowVersion{
		Id:     "v_1",
		Status: models.FlowStatusDraft,
	})
	require.NoError(t, err)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_def_1")
	assert.True(t, metaRepo.flowDefinitions["flow_def_1"].IsCustomized)
}

func TestMetadataService_DeleteFlowVersion_NonLastVersion_MarksCustomized(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_def_1"] = &models.FlowDefinition{Id: "flow_def_1"}
	metaRepo.flowVersions["v_1"] = &models.FlowVersion{
		Id:           "v_1",
		DefinitionId: "flow_def_1",
	}
	metaRepo.flowVersions["v_2"] = &models.FlowVersion{
		Id:           "v_2",
		DefinitionId: "flow_def_1",
	}

	err := ms.DeleteFlowVersion(ctx, "v_1")
	require.NoError(t, err)
	assertMarkedFlowDefinitions(t, metaRepo, "flow_def_1")
	assert.True(t, metaRepo.flowDefinitions["flow_def_1"].IsCustomized)
}

// ============================================================================
// Tests: Validation Rule CRUD
// ============================================================================

func TestMetadataService_CreateValidationRule_HappyPath(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	rule := &models.ValidationRule{
		ObjectAPIName: "Account",
		Name:          "Require_Name",
		Condition:     "Name == null",
		ErrorMessage:  "Name is required",
		Active:        true,
	}

	err := ms.CreateValidationRule(ctx, rule)
	require.NoError(t, err)
	assert.NotEmpty(t, rule.ID)
	assert.Contains(t, metaRepo.validationRuleByID, rule.ID)
}

func TestMetadataService_CreateValidationRule_MissingFields(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	tests := []struct {
		name   string
		rule   *models.ValidationRule
		errMsg string
	}{
		{
			name:   "missing object name",
			rule:   &models.ValidationRule{Name: "R1", Condition: "x == 1", ErrorMessage: "err"},
			errMsg: "object API name is required",
		},
		{
			name:   "missing rule name",
			rule:   &models.ValidationRule{ObjectAPIName: "Account", Condition: "x == 1", ErrorMessage: "err"},
			errMsg: "rule name is required",
		},
		{
			name:   "missing condition",
			rule:   &models.ValidationRule{ObjectAPIName: "Account", Name: "R1", ErrorMessage: "err"},
			errMsg: "error condition formula is required",
		},
		{
			name:   "missing error message",
			rule:   &models.ValidationRule{ObjectAPIName: "Account", Name: "R1", Condition: "x == 1"},
			errMsg: "error message is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ms.CreateValidationRule(ctx, tt.rule)
			require.Error(t, err)
			assert.Contains(t, err.Error(), tt.errMsg)
		})
	}
}

func TestMetadataService_CreateValidationRule_DuplicateName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.validationRuleByID["rule_1"] = &models.ValidationRule{
		ID:            "rule_1",
		ObjectAPIName: "account",
		Name:          "Require_Name",
	}

	rule := &models.ValidationRule{
		ObjectAPIName: "Account",
		Name:          "Require_Name",
		Condition:     "Name == null",
		ErrorMessage:  "Name is required",
	}

	err := ms.CreateValidationRule(ctx, rule)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_UpdateValidationRule_HappyPath(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.validationRuleByID["rule_1"] = &models.ValidationRule{
		ID:            "rule_1",
		ObjectAPIName: "account",
		Name:          "Require_Name",
		Condition:     "Name == null",
		ErrorMessage:  "Name is required",
		Active:        true,
	}

	updates := &models.ValidationRule{
		ErrorMessage: "Please enter a name",
		Active:       false,
	}

	err := ms.UpdateValidationRule(ctx, "rule_1", updates)
	require.NoError(t, err)
}

func TestMetadataService_UpdateValidationRule_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.UpdateValidationRule(ctx, "nonexistent", &models.ValidationRule{ErrorMessage: "new"})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_DeleteValidationRule(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.validationRuleByID["rule_1"] = &models.ValidationRule{ID: "rule_1"}

	err := ms.DeleteValidationRule(ctx, "rule_1")
	require.NoError(t, err)
	assert.NotContains(t, metaRepo.validationRuleByID, "rule_1")
}

func TestMetadataService_DeleteValidationRule_Error(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.deleteValidationRuleErr = fmt.Errorf("foreign key constraint")

	err := ms.DeleteValidationRule(ctx, "rule_1")
	assert.Error(t, err)
}

func TestMetadataService_GetValidationRules_SkipsSystemTables(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	// Prepopulate the cache with a schema (to avoid errors)
	_ = ms.RefreshCache(ctx) // Will build with empty schemas, which is OK

	rules := ms.GetValidationRules(ctx, "_System_Object")
	assert.Empty(t, rules)
}

// ============================================================================
// Tests: ValidateMaxMasterDetailFields
// ============================================================================

func TestMetadataService_ValidateMaxMasterDetailFields(t *testing.T) {
	ms, _, _, _ := newTestService()

	tests := []struct {
		name      string
		fields    []models.FieldMetadata
		expectErr bool
	}{
		{
			name: "zero master-detail",
			fields: []models.FieldMetadata{
				{APIName: "Name", Type: constants.FieldTypeString},
			},
			expectErr: false,
		},
		{
			name: "one master-detail",
			fields: []models.FieldMetadata{
				{APIName: "ParentId", Type: constants.FieldTypeReference, IsMasterDetail: true},
			},
			expectErr: false,
		},
		{
			name: "two master-detail - at limit",
			fields: []models.FieldMetadata{
				{APIName: "Parent1Id", Type: constants.FieldTypeReference, IsMasterDetail: true},
				{APIName: "Parent2Id", Type: constants.FieldTypeReference, IsMasterDetail: true},
			},
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj := &models.ObjectMetadata{
				APIName: "TestObj",
				Fields:  tt.fields,
			}
			err := ms.ValidateMaxMasterDetailFields(obj, "")
			if tt.expectErr {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), "maximum of 2")
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestMetadataService_CreateListView(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	view := &models.ListView{
		ObjectAPIName: "Account",
		Label:         "My View",
		Fields:        []string{"Name", "Industry"},
	}

	err := ms.CreateListView(ctx, view)
	require.NoError(t, err)
	assert.NotEmpty(t, view.ID) // ID should be generated
	assert.Len(t, metaRepo.listViews["account"], 1)
}

func TestMetadataService_UpdateListView(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.UpdateListView(ctx, "lv_1", &models.ListView{Label: "Updated"})
	require.NoError(t, err)
}

func TestMetadataService_DeleteListView(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	err := ms.DeleteListView(ctx, "lv_1")
	require.NoError(t, err)
}

// ============================================================================
// Tests: Object Getters (Delegate to Repo)
// ============================================================================

func TestMetadataService_GetAllObjects(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account"}
	metaRepo.schemas["contact"] = &models.ObjectMetadata{APIName: "Contact"}

	user := &models.UserSession{ID: "user_1", ProfileID: "admin"}
	objects, err := ms.GetAllObjects(ctx, user)
	require.NoError(t, err)
	assert.Len(t, objects, 2)
}

func TestMetadataService_GetSystemFields(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Id", IsSystem: true},
			{APIName: "CreatedDate", IsSystem: true},
			{APIName: "Name", IsNameField: true},
			{APIName: "Industry"},
		},
	}

	sysFields := ms.GetSystemFields(ctx, "Account")
	assert.Len(t, sysFields, 3) // Id, CreatedDate, Name (IsNameField counts)
	assert.Contains(t, sysFields, "Id")
	assert.Contains(t, sysFields, "CreatedDate")
	assert.Contains(t, sysFields, "Name")
	assert.NotContains(t, sysFields, "Industry")
}

func TestMetadataService_GetSupportedEvents(t *testing.T) {
	ms, _, _, _ := newTestService()
	events := ms.GetSupportedEvents()
	assert.Len(t, events, 6)
}

func TestMetadataService_GetListViews(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.listViews["account"] = []*models.ListView{
		{ID: "lv_1", Label: "All"},
		{ID: "lv_2", Label: "My Accounts"},
	}

	views := ms.GetListViews(ctx, "Account")
	assert.Len(t, views, 2)
}

func TestMetadataService_GetSharingRules(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.sharingRules["account"] = []*models.SharingRule{
		{ID: "sr_1", ObjectAPIName: "Account"},
	}

	rules := ms.GetSharingRules(ctx, "Account")
	assert.Len(t, rules, 1)
}

func TestMetadataService_GetActions(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.actionsByObj["account"] = []*models.ActionMetadata{
		{ID: "a_1", Name: "Action1"},
	}

	actions := ms.GetActions(ctx, "Account")
	assert.Len(t, actions, 1)
}

func TestMetadataService_GetActionByID(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.actions["a_1"] = &models.ActionMetadata{ID: "a_1", Name: "Action1"}

	result := ms.GetActionByID(ctx, "a_1")
	require.NotNil(t, result)
	assert.Equal(t, "Action1", result.Name)
}

func TestMetadataService_GetActionByID_NotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetActionByID(ctx, "nonexistent")
	assert.Nil(t, result)
}

func TestMetadataService_GetAutoNumbers_FromCache(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account"}
	metaRepo.autoNumbers["account"] = []*models.AutoNumber{
		{ID: "an_1", ObjectAPIName: "Account", FieldAPIName: "AccountNumber"},
	}

	result := ms.GetAutoNumbers(ctx, "Account")
	assert.Len(t, result, 1)
}

func TestMetadataService_GetAutoNumbers_UnknownObject(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result := ms.GetAutoNumbers(ctx, "NonExistent")
	assert.Empty(t, result)
}

// ============================================================================
// Tests: GenerateDefaultLayout
// ============================================================================

func TestMetadataService_GenerateDefaultLayout(t *testing.T) {
	ms, _, _, _ := newTestService()

	schema := &models.ObjectMetadata{
		APIName: "TestObj",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "CreatedDate", Label: "Created", Type: constants.FieldTypeDatetime, IsSystem: true},
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Field1", Label: "Field 1", Type: constants.FieldTypeString},
			{APIName: "Field2", Label: "Field 2", Type: constants.FieldTypeString},
			{APIName: "Field3", Label: "Field 3", Type: constants.FieldTypeString},
			{APIName: "Field4", Label: "Field 4", Type: constants.FieldTypeString},
			{APIName: "Field5", Label: "Field 5", Type: constants.FieldTypeString},
			{APIName: "Field6", Label: "Field 6", Type: constants.FieldTypeString},
			{APIName: "Field7", Label: "Field 7", Type: constants.FieldTypeString},
			{APIName: "Field8", Label: "Field 8", Type: constants.FieldTypeString},
		},
	}

	layout := ms.GenerateDefaultLayout(schema)

	// Verify structure
	assert.Equal(t, "TestObj", layout.ObjectAPIName)
	assert.Equal(t, constants.LayoutNameDefault, layout.LayoutName)
	assert.Equal(t, "Detail", layout.Type)
	assert.NotEmpty(t, layout.ID)

	// Verify sections
	require.Len(t, layout.Sections, 1)
	assert.Equal(t, constants.SectionLabelInformation, layout.Sections[0].Label)
	assert.Equal(t, 2, layout.Sections[0].Columns)

	// System fields (Id, CreatedDate) should NOT be in layout fields
	for _, f := range layout.Sections[0].Fields {
		assert.NotEqual(t, "Id", f)
		assert.NotEqual(t, "CreatedDate", f)
	}

	// Non-system fields should be in layout
	assert.Contains(t, layout.Sections[0].Fields, "Name")
	assert.Contains(t, layout.Sections[0].Fields, "Field1")

	// Compact layout should have max 7 fields
	assert.LessOrEqual(t, len(layout.CompactLayout), 7)
}

// ============================================================================
// Tests: EnrichWithSystemFields
// ============================================================================

func TestEnrichWithSystemFields(t *testing.T) {
	standardFields := []models.FieldMetadata{
		{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
		{APIName: "CreatedDate", Label: "Created", Type: constants.FieldTypeDatetime, IsSystem: true},
	}

	t.Run("adds missing system fields", func(t *testing.T) {
		schema := &models.ObjectMetadata{
			Fields: []models.FieldMetadata{
				{APIName: "CustomField", Label: "Custom", Type: constants.FieldTypeString},
			},
		}

		EnrichWithSystemFields(schema, standardFields)

		// Should now have system fields + custom field + auto-added Name
		assert.GreaterOrEqual(t, len(schema.Fields), 3)

		// System fields should be prepended
		assert.Equal(t, "Id", schema.Fields[0].APIName)
		assert.Equal(t, "CreatedDate", schema.Fields[1].APIName)
	})

	t.Run("does not duplicate existing system fields", func(t *testing.T) {
		schema := &models.ObjectMetadata{
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
				{APIName: "CustomField", Label: "Custom", Type: constants.FieldTypeString},
			},
		}

		EnrichWithSystemFields(schema, standardFields)

		// Count Id fields
		idCount := 0
		for _, f := range schema.Fields {
			if f.APIName == "Id" {
				idCount++
			}
		}
		assert.Equal(t, 1, idCount, "Should not duplicate Id field")
	})

	t.Run("adds default Name field if missing", func(t *testing.T) {
		schema := &models.ObjectMetadata{
			Fields: []models.FieldMetadata{
				{APIName: "CustomField", Label: "Custom", Type: constants.FieldTypeString},
			},
		}

		EnrichWithSystemFields(schema, standardFields)

		hasName := false
		for _, f := range schema.Fields {
			if f.APIName == "Name" && f.IsNameField {
				hasName = true
				break
			}
		}
		assert.True(t, hasName, "Should add default Name field when missing")
	})

	t.Run("does not add Name if IsNameField already set", func(t *testing.T) {
		schema := &models.ObjectMetadata{
			Fields: []models.FieldMetadata{
				{APIName: "Title", Label: "Title", Type: constants.FieldTypeString, IsNameField: true},
			},
		}

		EnrichWithSystemFields(schema, standardFields)

		nameCount := 0
		for _, f := range schema.Fields {
			if f.APIName == "Name" {
				nameCount++
			}
		}
		assert.Equal(t, 0, nameCount, "Should not add Name field when IsNameField already exists")
	})
}

// ============================================================================
// Tests: PrepareTableDefinition
// ============================================================================

func TestMetadataService_PrepareTableDefinition_HappyPath(t *testing.T) {
	ms, _, _, _ := newTestService()

	schema := &models.ObjectMetadata{
		APIName:     "CustomObj",
		Label:       "Custom Object",
		PluralLabel: "Custom Objects",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true, Required: true},
			{APIName: "Amount", Label: "Amount", Type: constants.FieldTypeCurrency},
		},
	}

	def, batchFields, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)

	assert.Equal(t, "CustomObj", def.TableName)
	assert.NotEmpty(t, def.Columns)

	// Should have system fields + user fields
	assert.Greater(t, len(def.Columns), 2)

	// Schema should have ID set
	assert.NotEmpty(t, schema.ID)

	// Batch fields for metadata registration
	assert.NotEmpty(t, batchFields)
}

func TestMetadataService_PrepareTableDefinition_InvalidAPIName(t *testing.T) {
	ms, _, _, _ := newTestService()

	tests := []struct {
		name    string
		apiName string
	}{
		{"spaces", "bad name"},
		{"dashes", "bad-name"},
		{"starts with number", "1BadName"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			schema := &models.ObjectMetadata{
				APIName: tt.apiName,
				Label:   "Test",
			}
			_, _, err := ms.PrepareTableDefinition(schema)
			assert.Error(t, err)
			assert.Contains(t, err.Error(), "invalid API name")
		})
	}
}

// BatchSyncSystemFields tests removed — function deprecated (no production callers).
// Field capability is now determined by _System_Field.Createable/Updateable.

// ============================================================================
// Tests: BatchCreateFields
// ============================================================================

func TestMetadataService_BatchCreateFields_Empty(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	result, err := ms.BatchCreateFields(ctx, "Account", []models.FieldMetadata{})
	require.NoError(t, err)
	assert.Nil(t, result)
}

func TestMetadataService_BatchCreateFields_ObjectNotFound(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	fields := []models.FieldMetadata{
		{APIName: "Field1", Label: "Field 1", Type: constants.FieldTypeString},
	}

	_, err := ms.BatchCreateFields(ctx, "NonExistent", fields)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_BatchCreateFields_HappyPath(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	fields := []models.FieldMetadata{
		{APIName: "Industry", Label: "Industry", Type: constants.FieldTypeString},
		{APIName: "Revenue", Label: "Revenue", Type: constants.FieldTypeCurrency},
	}

	result, err := ms.BatchCreateFields(ctx, "Account", fields)
	require.NoError(t, err)
	assert.Len(t, result, 2)

	// Verify columns added via schema repo
	assert.Len(t, schemaRepo.addedColumns["Account"], 2)
}

func TestMetadataService_BatchCreateFields_InvalidFieldName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:      "obj_account",
		APIName: "Account",
		Fields:  []models.FieldMetadata{},
	}

	fields := []models.FieldMetadata{
		{APIName: "bad-field", Label: "Bad", Type: constants.FieldTypeString},
	}

	_, err := ms.BatchCreateFields(ctx, "Account", fields)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid field API name")
}

// ============================================================================
// Tests: isSystemTableForCaching
// ============================================================================

func TestIsSystemTableForCaching(t *testing.T) {
	tests := []struct {
		name     string
		apiName  string
		expected bool
	}{
		{"system object", "_System_Object", true},
		{"system field", "_System_Field", true},
		{"regular object", "Account", false},
		{"custom object", "CustomObj__c", false},
		{"partial match", "System_Obj", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := isSystemTableForCaching(tt.apiName)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// ============================================================================
// Tests: Event Publishing
// ============================================================================

func TestMetadataService_PublishMetadataEvent_NilEventBus(t *testing.T) {
	metaRepo := newMockMetadataRepo()
	schemaRepo := newMockSchemaRepo()
	schemaMgr := NewSchemaManager(schemaRepo)
	// No event bus (nil)
	ms := NewMetadataService(metaRepo, schemaMgr, nil)

	ctx := context.Background()
	// Should not panic
	ms.publishMetadataEvent(ctx, sharedEvents.ObjectCreated, nil)
}

func TestMetadataService_PublishMetadataEvent_Error(t *testing.T) {
	ms, _, _, eventPub := newTestService()
	ctx := context.Background()

	eventPub.publishErr = fmt.Errorf("event bus down")

	// Should not panic, just log
	ms.publishMetadataEvent(ctx, sharedEvents.ObjectCreated, nil)
}

// ============================================================================
// Tests: CreateSchemaOptimized
// ============================================================================

func TestMetadataService_CreateSchemaOptimized_HappyPath(t *testing.T) {
	ms, metaRepo, schemaRepo, eventPub := newTestService()
	ctx := context.Background()

	schema := &models.ObjectMetadata{
		APIName:     "OptimizedObj",
		Label:       "Optimized Object",
		PluralLabel: "Optimized Objects",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	err := ms.CreateSchemaOptimized(ctx, schema)
	require.NoError(t, err)

	assert.Contains(t, schemaRepo.createdTables, "OptimizedObj")
	assert.NotEmpty(t, metaRepo.layoutByID)
	require.Len(t, eventPub.published, 1)
	assert.Equal(t, sharedEvents.ObjectCreated, eventPub.published[0].eventType)
}

func TestMetadataService_CreateSchemaOptimized_DuplicateAPIName(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["existingobj"] = &models.ObjectMetadata{
		APIName: "ExistingObj",
	}

	schema := &models.ObjectMetadata{
		APIName:     "ExistingObj",
		Label:       "Dup",
		PluralLabel: "Dups",
	}

	err := ms.CreateSchemaOptimized(ctx, schema)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

// ============================================================================
// Tests: UpdateSchema merge logic
// ============================================================================

func TestMetadataService_UpdateSchema_PartialMerge(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	desc := "Original description"
	color := "#FF0000"
	appID := "app_1"

	metaRepo.schemas["testobj"] = &models.ObjectMetadata{
		ID:           "obj_testobj",
		APIName:      "TestObj",
		Label:        "Original Label",
		PluralLabel:  "Original Plurals",
		Description:  &desc,
		Icon:         "old_icon",
		SharingModel: constants.SharingModelPrivate,
		ThemeColor:   &color,
		AppID:        &appID,
	}

	// Only update Label and Icon
	updates := &models.ObjectMetadata{
		Label: "New Label",
		Icon:  "new_icon",
	}

	err := ms.UpdateSchema(ctx, "TestObj", updates)
	require.NoError(t, err)

	// Verify the saved object has merged fields
	require.NotEmpty(t, schemaRepo.savedObjects)
	saved := schemaRepo.savedObjects[0]
	assert.Equal(t, "New Label", saved.Label)
	assert.Equal(t, "Original Plurals", saved.PluralLabel) // Unchanged
	assert.Equal(t, "new_icon", saved.Icon)
	assert.Equal(t, &desc, saved.Description) // Unchanged
	assert.True(t, saved.IsCustomized)
}

func TestMetadataService_UpdateSchema_MarksCustomizedForSharingModelChange(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["testobj"] = &models.ObjectMetadata{
		ID:           "obj-1",
		APIName:      "TestObj",
		Label:        "Original",
		PluralLabel:  "Originals",
		SharingModel: constants.SharingModelPrivate,
	}

	updates := &models.ObjectMetadata{
		SharingModel: constants.SharingModelPublicRead,
	}

	err := ms.UpdateSchema(ctx, "TestObj", updates)
	require.NoError(t, err)

	require.NotEmpty(t, schemaRepo.savedObjects)
	saved := schemaRepo.savedObjects[0]
	assert.Equal(t, constants.SharingModelPublicRead, saved.SharingModel)
	assert.True(t, saved.IsCustomized)
}

func TestMetadataService_UpdateSchema_MarksCustomizedForObjectMetadataChange(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	desc := "Original description"
	path := "StageName"
	metaRepo.schemas["testobj"] = &models.ObjectMetadata{
		ID:           "obj-1",
		APIName:      "TestObj",
		Label:        "Original",
		PluralLabel:  "Originals",
		Description:  &desc,
		PathField:    &path,
		SharingModel: constants.SharingModelPrivate,
	}

	newDesc := "Admin description"
	newPath := "Status"
	updates := &models.ObjectMetadata{
		Description: &newDesc,
		PathField:   &newPath,
	}

	err := ms.UpdateSchema(ctx, "TestObj", updates)
	require.NoError(t, err)

	require.NotEmpty(t, schemaRepo.savedObjects)
	saved := schemaRepo.savedObjects[0]
	assert.Equal(t, &newDesc, saved.Description)
	assert.Equal(t, &newPath, saved.PathField)
	assert.True(t, saved.IsCustomized)
}

func TestMetadataService_UpdateSchema_MarksCustomizedForListFields(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["testobj"] = &models.ObjectMetadata{
		ID:          "obj-1",
		APIName:     "TestObj",
		Label:       "Original",
		PluralLabel: "Originals",
		ListFields:  []string{"Name"},
	}

	updates := &models.ObjectMetadata{
		ListFields: []string{"Name", "Phone"},
	}

	err := ms.UpdateSchema(ctx, "TestObj", updates)
	require.NoError(t, err)

	require.NotEmpty(t, schemaRepo.savedObjects)
	saved := schemaRepo.savedObjects[0]
	assert.Equal(t, []string{"Name", "Phone"}, saved.ListFields)
	assert.True(t, saved.IsCustomized)
}

func TestMetadataService_UpdateSchema_DoesNotCustomizeOnNoOpSharingModel(t *testing.T) {
	ms, metaRepo, schemaRepo, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["testobj"] = &models.ObjectMetadata{
		ID:           "obj-1",
		APIName:      "TestObj",
		Label:        "Original",
		PluralLabel:  "Originals",
		SharingModel: constants.SharingModelPrivate,
	}

	updates := &models.ObjectMetadata{
		SharingModel: constants.SharingModelPrivate,
	}

	err := ms.UpdateSchema(ctx, "TestObj", updates)
	require.NoError(t, err)

	require.NotEmpty(t, schemaRepo.savedObjects)
	saved := schemaRepo.savedObjects[0]
	assert.Equal(t, constants.SharingModelPrivate, saved.SharingModel)
	assert.False(t, saved.IsCustomized)
}

func TestMetadataService_GetSchema_ReadsPersistedSharingModel(t *testing.T) {
	// Runtime no longer applies overlays — it reads persisted metadata as-is.
	// The control plane reconciler is responsible for ensuring correct values.
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:           "obj-account",
		APIName:      "Account",
		TableType:    constants.TableTypeStandardObject,
		SharingModel: constants.SharingModelPublicRead,
	}

	got := ms.GetSchema(ctx, "Account")
	require.NotNil(t, got)
	assert.Equal(t, constants.SharingModelPublicRead, got.SharingModel)
}

func TestMetadataService_GetSchema_PreservesCustomizedSharingModel(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:           "obj-account",
		APIName:      "Account",
		TableType:    constants.TableTypeStandardObject,
		SharingModel: constants.SharingModelPublicReadWrite,
		IsCustomized: true,
	}

	got := ms.GetSchema(ctx, "Account")
	require.NotNil(t, got)
	assert.Equal(t, constants.SharingModelPublicReadWrite, got.SharingModel)
}

func TestMetadataService_GetSchema_PreservesCustomizedLegacySharingModel(t *testing.T) {
	// Runtime reads persisted values as-is — no overlay rewriting.
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:           "obj-account",
		APIName:      "Account",
		TableType:    constants.TableTypeStandardObject,
		SharingModel: constants.SharingModelPublicRead,
		IsCustomized: true,
	}

	got := ms.GetSchema(ctx, "Account")
	require.NotNil(t, got)
	assert.Equal(t, constants.SharingModelPublicRead, got.SharingModel)
}

func TestMetadataService_GetSchema_ReadsPersistedHierarchySharingWhenFullCacheInitFails(t *testing.T) {
	// Runtime reads persisted values — no overlay. If EnableHierarchySharing
	// is false in DB, runtime serves false. The control plane reconciler is
	// responsible for setting it correctly.
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["contract"] = &models.ObjectMetadata{
		ID:                     "obj-contract",
		APIName:                "Contract",
		TableType:              constants.TableTypeStandardObject,
		SharingModel:           constants.SharingModelPrivate,
		EnableHierarchySharing: false,
	}
	metaRepo.getAllFlowDefsErr = fmt.Errorf("flow load failed")

	got := ms.GetSchema(ctx, "Contract")
	require.NotNil(t, got)
	assert.False(t, got.EnableHierarchySharing)
	assert.Nil(t, ms.loadCache())
	assert.NotNil(t, ms.loadCoreCache())
}

func TestMetadataService_GetRelationships_ReadsPersistedSharingParentSemantics(t *testing.T) {
	// Runtime reads persisted relationship semantics as-is — no overlay.
	// If IsSharingParent=false in DB, runtime serves false. The control
	// plane reconciler is responsible for setting correct values.
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	orderRel := "Account"
	quoteRel := "Opportunity"
	metaRepo.schemas["order"] = &models.ObjectMetadata{
		ID:        "obj-order",
		APIName:   "Order",
		TableType: constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &orderRel},
		},
	}
	metaRepo.schemas["quote"] = &models.ObjectMetadata{
		ID:        "obj-quote",
		APIName:   "Quote",
		TableType: constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "OpportunityId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Opportunity"}, RelationshipName: &quoteRel},
		},
	}
	metaRepo.relationships["order"] = []*models.Relationship{
		{ID: "rel-order", ChildObjectAPIName: "Order", ParentObjectAPIName: "Account", FieldAPIName: "AccountId", IsSharingParent: false},
	}
	metaRepo.relationships["quote"] = []*models.Relationship{
		{ID: "rel-quote", ChildObjectAPIName: "Quote", ParentObjectAPIName: "Opportunity", FieldAPIName: "OpportunityId", IsSharingParent: false},
	}

	orderRelationships := ms.GetRelationships(ctx, "Order")
	require.Len(t, orderRelationships, 1)
	assert.False(t, orderRelationships[0].IsSharingParent)

	quoteRelationships := ms.GetRelationships(ctx, "Quote")
	require.Len(t, quoteRelationships, 1)
	assert.False(t, quoteRelationships[0].IsSharingParent)
}

func TestMetadataService_GetRelationships_UsesCoreCacheWhenFullCacheInitFails(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	orderRel := "Account"
	metaRepo.schemas["order"] = &models.ObjectMetadata{
		ID:        "obj-order",
		APIName:   "Order",
		TableType: constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &orderRel},
		},
	}
	metaRepo.getAllFlowDefsErr = fmt.Errorf("flow load failed")

	rels := ms.GetRelationships(ctx, "Order")
	require.Len(t, rels, 1)
	// IsSharingParent is now set by persisted data (via reconciler), not by
	// a runtime overlay. The mock repo has a plain Reference field, so the
	// field-derived default is false.
	assert.False(t, rels[0].IsSharingParent)
	assert.Nil(t, ms.loadCache())
	assert.NotNil(t, ms.loadCoreCache())
}

func TestMetadataService_GetSharingParentRelationship_ReadsPersistedSharingParentSemantics(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	orderRel := "Account"
	metaRepo.schemas["order"] = &models.ObjectMetadata{
		ID:        "obj-order",
		APIName:   "Order",
		TableType: constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &orderRel},
		},
	}
	metaRepo.relationships["order"] = []*models.Relationship{
		{ID: "rel-order", ChildObjectAPIName: "Order", ParentObjectAPIName: "Account", FieldAPIName: "AccountId", IsSharingParent: true},
	}

	rel := ms.GetSharingParentRelationship(ctx, "Order")
	require.NotNil(t, rel)
	assert.Equal(t, "Account", rel.ParentObjectAPIName)
	assert.Equal(t, "AccountId", rel.FieldAPIName)

	metaRepo.relationships["order"] = []*models.Relationship{
		{ID: "rel-order", ChildObjectAPIName: "Order", ParentObjectAPIName: "Account", FieldAPIName: "AccountId", IsSharingParent: false},
	}
	ms.InvalidateCache()

	rel = ms.GetSharingParentRelationship(ctx, "Order")
	assert.Nil(t, rel)
}

func TestMetadataService_GetSharingParentRelationship_UsesCoreCacheWhenFullCacheInitFails(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	orderRel := "Account"
	metaRepo.schemas["order"] = &models.ObjectMetadata{
		ID:        "obj-order",
		APIName:   "Order",
		TableType: constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &orderRel},
		},
	}
	metaRepo.relationships["order"] = []*models.Relationship{
		{ID: "rel-order", ChildObjectAPIName: "Order", ParentObjectAPIName: "Account", FieldAPIName: "AccountId", IsSharingParent: true},
	}
	metaRepo.getAllFlowDefsErr = fmt.Errorf("flow load failed")

	rel := ms.GetSharingParentRelationship(ctx, "Order")
	require.NotNil(t, rel)
	assert.Equal(t, "Account", rel.ParentObjectAPIName)
	assert.Nil(t, ms.loadCache())
	assert.NotNil(t, ms.loadCoreCache())
}

func TestMetadataService_GetChildRelationships_UsesCoreCacheWhenFullCacheInitFails(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	accountRel := "Account"
	metaRepo.schemas["account"] = &models.ObjectMetadata{
		ID:        "obj-account",
		APIName:   "Account",
		TableType: constants.TableTypeStandardObject,
	}
	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:        "obj-contact",
		APIName:   "Contact",
		TableType: constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &accountRel},
		},
	}
	metaRepo.getAllFlowDefsErr = fmt.Errorf("flow load failed")

	children := ms.GetChildRelationships(ctx, "Account")
	require.Len(t, children, 1)
	assert.Equal(t, "Contact", children[0].APIName)
	assert.Nil(t, ms.loadCache())
	assert.NotNil(t, ms.loadCoreCache())
}

// ============================================================================
// Tests: Flow Version auto-increment VersionNumber (issue #91)
// ============================================================================

func TestCreateFlowVersion_AutoIncrementsVersionNumber(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	defId := "def-001"
	metaRepo.flowDefinitions[defId] = &models.FlowDefinition{
		Id:            defId,
		DeveloperName: "Test_Flow",
		MasterLabel:   "Test Flow",
	}

	// Create first version with no VersionNumber set (simulates frontend POST)
	v1 := &models.FlowVersion{
		DefinitionId: defId,
		Status:       "Active",
	}
	err := ms.CreateFlowVersion(ctx, v1)
	require.NoError(t, err)
	assert.Equal(t, 1, v1.VersionNumber, "first version should be 1")

	// Create second version (Save As New Version) — also no VersionNumber
	v2 := &models.FlowVersion{
		DefinitionId: defId,
		Status:       "Draft",
	}
	err = ms.CreateFlowVersion(ctx, v2)
	require.NoError(t, err)
	assert.Equal(t, 2, v2.VersionNumber, "second version should auto-increment to 2")

	// Create third version
	v3 := &models.FlowVersion{
		DefinitionId: defId,
		Status:       "Draft",
	}
	err = ms.CreateFlowVersion(ctx, v3)
	require.NoError(t, err)
	assert.Equal(t, 3, v3.VersionNumber, "third version should auto-increment to 3")
}

func TestCreateFlowVersion_ReturnsErrorWhenQueryFails(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.getFlowVersionsErr = fmt.Errorf("database connection lost")

	v := &models.FlowVersion{
		DefinitionId: "def-001",
		Status:       "Draft",
	}
	err := ms.CreateFlowVersion(ctx, v)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to query existing versions for auto-increment")
	assert.Contains(t, err.Error(), "database connection lost")
}

func TestCreateFlowVersion_FirstVersionDefaultsTo1(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	v := &models.FlowVersion{
		DefinitionId: "def-new",
		Status:       "Draft",
	}
	err := ms.CreateFlowVersion(ctx, v)
	require.NoError(t, err)
	assert.Equal(t, 1, v.VersionNumber, "first version for a new definition should be 1")
}

// --- Layout Assignment interface stubs (WP2) ---

// FindBestAssignment simulates assignment lookup by checking if the mock has
// layouts stored for the requested object. If so, returns a fake assignment
// pointing to the first (default) layout — this makes existing tests that
// populate metaRepo.layouts work with the new ResolveLayout path.
// Also registers the layout in layoutByID so GetLayout(id) can find it.
func (m *mockMetadataRepo) FindBestAssignment(_ context.Context, objectAPIName string, _ []string, _ string, _ string) (*models.LayoutAssignment, error) {
	key := strings.ToLower(objectAPIName)
	if assignments := m.assignmentsByObject[key]; len(assignments) > 0 {
		return assignments[0], nil
	}
	if m.disableImplicitLayoutAssignment {
		return nil, nil
	}
	if layouts, ok := m.layouts[key]; ok && len(layouts) > 0 {
		// Ensure GetLayout(id) can find this layout.
		// Set Source if empty so regeneration doesn't trigger on test layouts
		// that set LayoutVersion to currentLayoutVersion.
		l := layouts[0]
		if l.Source == "" {
			if l.IsCustomized {
				l.Source = "admin_created"
			} else {
				l.Source = "auto_generated"
			}
		}
		if l.GenerationVersion == 0 && l.LayoutVersion > 0 {
			l.GenerationVersion = l.LayoutVersion
		}
		m.layoutByID[l.ID] = l
		return &models.LayoutAssignment{
			ID:            "mock_assignment_" + layouts[0].ID,
			ObjectAPIName: objectAPIName,
			LayoutID:      layouts[0].ID,
			Specificity:   0,
		}, nil
	}
	return nil, nil
}

func (m *mockMetadataRepo) CreateAssignment(_ context.Context, assignment *models.LayoutAssignment) error {
	m.upsertAssignment(assignment)
	return nil
}

func (m *mockMetadataRepo) DeleteAssignment(_ context.Context, id string) error {
	m.deletedAssignmentIDs = append(m.deletedAssignmentIDs, id)
	for key, assignments := range m.assignmentsByObject {
		filtered := assignments[:0]
		for _, assignment := range assignments {
			if assignment.ID != id {
				filtered = append(filtered, assignment)
			}
		}
		m.assignmentsByObject[key] = filtered
	}
	return nil
}

func (m *mockMetadataRepo) GetAssignmentsForObject(_ context.Context, objectAPIName string) ([]*models.LayoutAssignment, error) {
	return append([]*models.LayoutAssignment(nil), m.assignmentsByObject[strings.ToLower(objectAPIName)]...), nil
}

func (m *mockMetadataRepo) DeleteAssignmentsByLayoutID(_ context.Context, layoutID string) error {
	for key, assignments := range m.assignmentsByObject {
		filtered := assignments[:0]
		for _, assignment := range assignments {
			if assignment.LayoutID != layoutID {
				filtered = append(filtered, assignment)
			}
		}
		m.assignmentsByObject[key] = filtered
	}
	return nil
}
func (m *mockMetadataRepo) WriteLayoutHistory(_ context.Context, _ *models.PageLayoutHistoryEntry) error {
	return nil
}
func (m *mockMetadataRepo) GetLayoutHistoryMaxVersion(_ context.Context, _ string) (int, error) {
	return 0, nil
}
func (m *mockMetadataRepo) GetMigrationVersion(_ context.Context, _ string) (int, error) {
	return 0, nil
}
func (m *mockMetadataRepo) SetMigrationVersion(_ context.Context, _ string, _ int, _ string) error {
	return nil
}

func (m *mockMetadataRepo) GetLayoutHistoryConfig(_ context.Context, _ string, _ int) (string, error) {
	return "", nil
}

func (m *mockMetadataRepo) PruneLayoutHistory(_ context.Context, _ string, _ int) error { return nil }

func (m *mockMetadataRepo) storeLayout(layout *models.PageLayout) {
	if layout == nil {
		return
	}

	key := strings.ToLower(layout.ObjectAPIName)
	m.layoutByID[layout.ID] = layout

	layouts := m.layouts[key]
	for i, existing := range layouts {
		if existing.ID == layout.ID {
			layouts[i] = layout
			m.layouts[key] = layouts
			return
		}
	}

	m.layouts[key] = append(layouts, layout)
}

func (m *mockMetadataRepo) upsertAssignment(assignment *models.LayoutAssignment) {
	if assignment == nil {
		return
	}

	key := strings.ToLower(assignment.ObjectAPIName)
	assignments := m.assignmentsByObject[key]
	for i, existing := range assignments {
		if sameAssignmentScope(existing, assignment) {
			assignments[i] = assignment
			m.assignmentsByObject[key] = assignments
			return
		}
	}

	m.assignmentsByObject[key] = append(assignments, assignment)
}

func sameAssignmentScope(left *models.LayoutAssignment, right *models.LayoutAssignment) bool {
	if left == nil || right == nil {
		return false
	}

	return sameOptionalString(left.RoleID, right.RoleID) &&
		sameOptionalString(left.ProfileID, right.ProfileID) &&
		sameOptionalString(left.RecordTypeID, right.RecordTypeID)
}

func sameOptionalString(left *string, right *string) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return *left == *right
}

// Mock methods for ApprovalPageLayout
func (m *mockMetadataRepo) GetApprovalLayouts(_ context.Context, objectAPIName string) ([]*models.ApprovalPageLayout, error) {
	if m.approvalLayouts == nil {
		return []*models.ApprovalPageLayout{}, nil
	}
	return m.approvalLayouts[strings.ToLower(objectAPIName)], nil
}

func (m *mockMetadataRepo) GetApprovalLayout(_ context.Context, id string) (*models.ApprovalPageLayout, error) {
	if m.approvalLayoutByID == nil {
		return nil, fmt.Errorf("not found")
	}
	l, ok := m.approvalLayoutByID[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return l, nil
}

func (m *mockMetadataRepo) SaveApprovalLayout(_ context.Context, layout *models.ApprovalPageLayout) error {
	if m.saveApprovalLayoutErr != nil {
		return m.saveApprovalLayoutErr
	}
	if m.approvalLayouts == nil {
		m.approvalLayouts = make(map[string][]*models.ApprovalPageLayout)
	}
	if m.approvalLayoutByID == nil {
		m.approvalLayoutByID = make(map[string]*models.ApprovalPageLayout)
	}

	key := strings.ToLower(layout.ObjectAPIName)
	m.approvalLayoutByID[layout.ID] = layout

	layouts := m.approvalLayouts[key]
	for i, existing := range layouts {
		if existing.ID == layout.ID {
			layouts[i] = layout
			m.approvalLayouts[key] = layouts
			return nil
		}
	}

	m.approvalLayouts[key] = append(layouts, layout)
	return nil
}

func (m *mockMetadataRepo) DeleteApprovalLayout(_ context.Context, id string) error {
	if m.deleteApprovalLayoutErr != nil {
		return m.deleteApprovalLayoutErr
	}
	if m.approvalLayoutByID == nil {
		return nil
	}
	delete(m.approvalLayoutByID, id)
	// Also remove from the arrays by object name
	for objKey, layouts := range m.approvalLayouts {
		newLayouts := []*models.ApprovalPageLayout{}
		for _, layout := range layouts {
			if layout.ID != id {
				newLayouts = append(newLayouts, layout)
			}
		}
		m.approvalLayouts[objKey] = newLayouts
	}
	return nil
}

func (m *mockMetadataRepo) SetDefaultApprovalLayout(_ context.Context, objectAPIName, layoutID string) error {
	// For now, just return nil since it's a mock
	return nil
}
