package rest_test

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/interfaces/rest"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

// MockQueryService implements ports.QueryService
type MockQueryService struct {
	mock.Mock
}

func (m *MockQueryService) Query(ctx context.Context, req api.QueryRequest, user *models.UserSession) (*models.QueryResult, error) {
	args := m.Called(ctx, req, user)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.QueryResult), args.Error(1)
}

func (m *MockQueryService) QueryWithFilter(ctx context.Context, objectName string, filterExpr string, user *models.UserSession, sortField string, sortDir string, limit int) (*models.QueryResult, error) {
	args := m.Called(ctx, objectName, filterExpr, user, sortField, sortDir, limit)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.QueryResult), args.Error(1)
}

func (m *MockQueryService) RunAnalytics(ctx context.Context, analyticsQuery models.AnalyticsQuery, currentUser *models.UserSession) (interface{}, error) {
	args := m.Called(ctx, analyticsQuery, currentUser)
	return args.Get(0), args.Error(1)
}

func (m *MockQueryService) SearchSingleObject(ctx context.Context, objectName string, term string, searchFields []string, returnFields []string, limit int, offset int, orderBy string, orderDir string, filterExpr string, user *models.UserSession) ([]models.SObject, error) {
	args := m.Called(ctx, objectName, term, searchFields, returnFields, limit, offset, orderBy, orderDir, filterExpr, user)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.SObject), args.Error(1)
}

func (m *MockQueryService) GlobalSearch(ctx context.Context, term string, currentUser *models.UserSession) ([]models.SearchResult, error) {
	args := m.Called(ctx, term, currentUser)
	return args.Get(0).([]models.SearchResult), args.Error(1)
}

func (m *MockQueryService) Calculate(ctx context.Context, objectName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	return nil, nil
}

// MockMetadataRepo implements ports.MetadataRepository (fully stubbed)
type MockMetadataRepo struct {
	mock.Mock
	flowDefinitions []*models.FlowDefinition
	flowVersions    map[string]*models.FlowVersion
}

func (m *MockMetadataRepo) GetSchemaByAPIName(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	args := m.Called(ctx, apiName)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.ObjectMetadata), args.Error(1)
}

func (m *MockMetadataRepo) GetAllSchemas(ctx context.Context) ([]*models.ObjectMetadata, error) {
	args := m.Called(ctx)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*models.ObjectMetadata), args.Error(1)
}

// Stubs for ALL interface methods to satisfy compilation
func (m *MockMetadataRepo) GetFieldsForObject(ctx context.Context, objectID string) ([]models.FieldMetadata, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetRecordTypes(ctx context.Context, objectAPIName string) ([]*models.RecordType, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAutoNumbers(ctx context.Context, objectAPIName string) ([]*models.AutoNumber, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAllAutoNumbers(_ context.Context) ([]*models.AutoNumber, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAllIdentifierRules(_ context.Context) ([]*models.IdentifierRule, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAllValidationRules(_ context.Context) ([]*models.ValidationRule, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAllRelationships(_ context.Context) ([]*models.Relationship, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetRelationships(ctx context.Context, childObjectAPIName string) ([]*models.Relationship, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetFieldDependencies(ctx context.Context, objectAPIName string) ([]*models.FieldDependency, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetChildRelationships(ctx context.Context, parentObjectAPIName string) ([]*models.ObjectMetadata, error) {
	return nil, nil
}
func (m *MockMetadataRepo) ConfigureAutoNumberField(ctx context.Context, objectAPIName, fieldAPIName, format string, start int64) error {
	return nil
}

func (m *MockMetadataRepo) UpsertUIComponent(ctx context.Context, component *models.UIComponent) error {
	return nil
}
func (m *MockMetadataRepo) UpsertSetupPage(ctx context.Context, page *models.SetupPage) error {
	return nil
}
func (m *MockMetadataRepo) GetSetupPages(ctx context.Context) ([]*models.SetupPage, error) {
	return nil, nil
}
func (m *MockMetadataRepo) UpsertLayout(ctx context.Context, layout *models.PageLayout) error {
	return nil
}
func (m *MockMetadataRepo) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	return nil
}
func (m *MockQueryService) ExecuteRawSQL(ctx context.Context, sql string, params []interface{}, user *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}
func (m *MockQueryService) MarkLayoutCustomized(_ context.Context, _ string) {}
func (m *MockMetadataRepo) GetLayout(ctx context.Context, id string) (*models.PageLayout, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	return nil, nil
}
func (m *MockMetadataRepo) DeleteLayout(ctx context.Context, id string) error { return nil }
func (m *MockMetadataRepo) SetDefaultLayout(ctx context.Context, objectAPIName string, layoutID string, actorID *string) error {
	return nil
}
func (m *MockMetadataRepo) AssignLayoutToProfile(ctx context.Context, profileID, objectAPIName, layoutID string) error {
	return nil
}
func (m *MockMetadataRepo) GetLayoutIDForProfile(ctx context.Context, objectAPIName, profileID string) (string, error) {
	return "", nil
}
func (m *MockMetadataRepo) GetRelatedListConfigs(ctx context.Context, layoutObjectAPIName string) ([]ports.RelatedListConfig, error) {
	return nil, nil
}

func (m *MockMetadataRepo) GetActiveTheme(ctx context.Context) (*models.Theme, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetThemeByName(ctx context.Context, name string) (*models.Theme, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateTheme(ctx context.Context, theme *models.Theme) error { return nil }
func (m *MockMetadataRepo) UpdateTheme(ctx context.Context, theme *models.Theme) error { return nil }
func (m *MockMetadataRepo) ActivateTheme(ctx context.Context, themeID string) error    { return nil }

func (m *MockMetadataRepo) GetAllApps(ctx context.Context) ([]*models.AppConfig, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetApp(ctx context.Context, id string) (*models.AppConfig, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAppWithTx(ctx context.Context, tx interface{}, id string) (*models.AppConfig, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateApp(ctx context.Context, app *models.AppConfig) error { return nil }
func (m *MockMetadataRepo) CreateAppMenuItem(ctx context.Context, app *models.AppConfig) error {
	return nil
}
func (m *MockMetadataRepo) UpdateApp(ctx context.Context, id string, app *models.AppConfig) error {
	return nil
}
func (m *MockMetadataRepo) UpdateAppWithTx(ctx context.Context, tx interface{}, id string, app *models.AppConfig) error {
	return nil
}
func (m *MockMetadataRepo) DeleteApp(ctx context.Context, id string) error { return nil }

func (m *MockMetadataRepo) GetAllDashboards(ctx context.Context) ([]*models.DashboardConfig, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetDashboard(ctx context.Context, id string) (*models.DashboardConfig, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateDashboard(ctx context.Context, dashboard *models.DashboardConfig) error {
	return nil
}
func (m *MockMetadataRepo) UpdateDashboard(ctx context.Context, id string, dashboard *models.DashboardConfig) error {
	return nil
}
func (m *MockMetadataRepo) DeleteDashboard(ctx context.Context, id string) error { return nil }

func (m *MockMetadataRepo) GetListViews(ctx context.Context, objectAPIName string) ([]*models.ListView, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateListView(ctx context.Context, lv *models.ListView) error { return nil }
func (m *MockMetadataRepo) UpdateListView(ctx context.Context, id string, lv *models.ListView) error {
	return nil
}
func (m *MockMetadataRepo) DeleteListView(ctx context.Context, id string) error { return nil }
func (m *MockMetadataRepo) CountListViews(ctx context.Context, objectAPIName string) (int, error) {
	return 0, nil
}

func (m *MockMetadataRepo) GetActions(ctx context.Context, objectAPIName string) ([]*models.ActionMetadata, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAction(ctx context.Context, id string) (*models.ActionMetadata, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetAllActions(ctx context.Context) ([]*models.ActionMetadata, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateAction(ctx context.Context, action *models.ActionMetadata) error {
	return nil
}
func (m *MockMetadataRepo) UpdateAction(ctx context.Context, actionID string, updates *models.ActionMetadata) error {
	return nil
}
func (m *MockMetadataRepo) DeleteAction(ctx context.Context, actionID string) error { return nil }
func (m *MockMetadataRepo) CheckActionExists(ctx context.Context, objectAPIName, name string) (bool, error) {
	return false, nil
}
func (m *MockMetadataRepo) GetSharingRules(ctx context.Context, objectAPIName string) ([]*models.SharingRule, error) {
	return nil, nil
}

// Salesforce-compatible FlowDefinition methods
func (m *MockMetadataRepo) GetAllFlowDefinitions(ctx context.Context) ([]*models.FlowDefinition, error) {
	if m.flowDefinitions != nil {
		return m.flowDefinitions, nil
	}
	return nil, nil
}
func (m *MockMetadataRepo) GetFlowDefinition(ctx context.Context, id string) (*models.FlowDefinition, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetFlowDefinitionByName(ctx context.Context, developerName string) (*models.FlowDefinition, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateFlowDefinition(ctx context.Context, def *models.FlowDefinition) error {
	return nil
}
func (m *MockMetadataRepo) UpdateFlowDefinition(ctx context.Context, id string, def *models.FlowDefinition) error {
	return nil
}
func (m *MockMetadataRepo) PatchFlowDefinition(ctx context.Context, id string, patch *models.FlowDefinitionPatch) error {
	args := m.Called(ctx, id, patch)
	return args.Error(0)
}
func (m *MockMetadataRepo) DeleteFlowDefinition(ctx context.Context, id string) error { return nil }

// Salesforce-compatible FlowVersion methods
func (m *MockMetadataRepo) GetFlowVersions(ctx context.Context, definitionId string) ([]*models.FlowVersion, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetFlowVersionsByIDs(_ context.Context, _ []string) (map[string]*models.FlowVersion, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetFlowVersion(ctx context.Context, id string) (*models.FlowVersion, error) {
	if m.flowVersions != nil {
		return m.flowVersions[id], nil
	}
	return nil, nil
}
func (m *MockMetadataRepo) GetActiveFlowVersion(ctx context.Context, definitionId string) (*models.FlowVersion, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	return nil
}
func (m *MockMetadataRepo) CreateSeededFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	return nil
}
func (m *MockMetadataRepo) CreateTenantFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	return nil
}
func (m *MockMetadataRepo) UpdateFlowVersion(ctx context.Context, id string, version *models.FlowVersion) error {
	return nil
}
func (m *MockMetadataRepo) ActivateFlowVersion(ctx context.Context, id string) error   { return nil }
func (m *MockMetadataRepo) DeactivateFlowVersion(ctx context.Context, id string) error { return nil }
func (m *MockMetadataRepo) DeleteFlowVersion(ctx context.Context, id string) error     { return nil }
func (m *MockMetadataRepo) SetLatestVersionId(ctx context.Context, definitionId string, versionId string) error {
	return nil
}
func (m *MockMetadataRepo) SetTriggerInfo(ctx context.Context, definitionId string, triggerType string, objectOrEvent string) error {
	return nil
}

// Salesforce-compatible FlowInterview methods
func (m *MockMetadataRepo) GetFlowInterviews(ctx context.Context) ([]*models.FlowInterview, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetFlowInterview(ctx context.Context, id string) (*models.FlowInterview, error) {
	return nil, nil
}
func (m *MockMetadataRepo) SaveFlowInterview(ctx context.Context, interview *models.FlowInterview) error {
	return nil
}
func (m *MockMetadataRepo) DeleteFlowInterview(ctx context.Context, id string) error { return nil }

func (m *MockMetadataRepo) GetValidationRules(ctx context.Context, objectAPIName string) ([]*models.ValidationRule, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetValidationRule(ctx context.Context, id string) (*models.ValidationRule, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateValidationRule(ctx context.Context, rule *models.ValidationRule) error {
	return nil
}
func (m *MockMetadataRepo) UpdateValidationRule(ctx context.Context, id string, updates *models.ValidationRule) error {
	return nil
}
func (m *MockMetadataRepo) DeleteValidationRule(ctx context.Context, id string) error { return nil }
func (m *MockMetadataRepo) CheckValidationRuleExists(ctx context.Context, objectAPIName, name string) (bool, error) {
	return false, nil
}
func (m *MockMetadataRepo) GetSchema(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	args := m.Called(ctx, apiName)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.ObjectMetadata), args.Error(1)
}
func (m *MockMetadataRepo) ValidateSchema(ctx context.Context, schema *models.ObjectMetadata) error {
	return nil
}
func (m *MockMetadataRepo) CreateSchema(ctx context.Context, schema *models.ObjectMetadata) error {
	return nil
}
func (m *MockMetadataRepo) UpdateSchema(ctx context.Context, schema *models.ObjectMetadata) error {
	return nil
}
func (m *MockMetadataRepo) DeleteSchema(ctx context.Context, apiName string) error { return nil }
func (m *MockMetadataRepo) GetSupportedEvents() []string                           { return nil }

// Tests are identical to previous version, just ensuring structs satisfy interface
func TestSearchHandler_SearchSOSL_StrictCasing(t *testing.T) {
	gin.SetMode(gin.TestMode)

	t.Run("Returns Strict Id Casing", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)

		authUser := auth.UserSession{
			ID:        "user1",
			ProfileId: constants.ProfileSystemAdmin,
		}
		c.Set(constants.ContextKeyUser, authUser)

		c.Request = httptest.NewRequest("GET", "/search?q=FIND+%7Btest%7D+RETURNING+Account", nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{
			QuerySvc:    mockQuery,
			Metadata:    metaSvc,
			Permissions: permSvc,
		}
		handler := rest.NewSearchHandler(svcMgr)

		// Fix: Add Fields so scope expansion works
		schema := &models.ObjectMetadata{
			APIName:    "Account",
			Searchable: true,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: constants.FieldTypeString},
				{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil)

		mockResults := []models.SObject{
			{"Id": "correct-123", "Name": "Test Account"},
		}
		mockQuery.On("SearchSingleObject", mock.Anything, "Account", "test", mock.Anything, mock.Anything, 500, 0, "", "", "", mock.Anything).Return(mockResults, nil)

		handler.SearchSOSL(c)

		assert.Equal(t, http.StatusOK, w.Code)
		var resp map[string]interface{}
		json.Unmarshal(w.Body.Bytes(), &resp)
		results := resp["searchRecords"].([]interface{})
		assert.NotEmpty(t, results)

		// Flat Salesforce format: [ { "Id": "...", "attributes": { "type": "Account", "url": "..." } } ]
		rec := results[0].(map[string]interface{})
		attrs := rec["attributes"].(map[string]interface{})
		assert.Equal(t, "Account", attrs["type"])
		url := attrs["url"].(string)
		assert.Contains(t, url, "/sobjects/Account/correct-123")
		mockQuery.AssertExpectations(t)
	})

	t.Run("Fails validation on legacy lower-case id", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
		c.Set(constants.ContextKeyUser, authUser)

		c.Request = httptest.NewRequest("GET", "/search?q=FIND+%7Btest%7D+RETURNING+Account", nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
		handler := rest.NewSearchHandler(svcMgr)

		// Fix: Add Fields so scope expansion works
		schema := &models.ObjectMetadata{
			APIName:    "Account",
			Searchable: true,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: constants.FieldTypeString},
				{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil)

		// Return ONLY lower-case "id" simulates a non-compliant DB or legacy data
		badResults := []models.SObject{
			{"id": "bad-case-123", "Name": "Test Account"}, // Note lowercase 'id'
		}
		mockQuery.On("SearchSingleObject", mock.Anything, "Account", "test", mock.Anything, mock.Anything, 500, 0, "", "", "", mock.Anything).Return(badResults, nil)

		handler.SearchSOSL(c)

		// The handler should still succeed but the "Id" lookup will be nil if strict pascal case is enforced
		assert.Equal(t, http.StatusOK, w.Code)
		var resp map[string]interface{}
		json.Unmarshal(w.Body.Bytes(), &resp)
		results := resp["searchRecords"].([]interface{})
		if len(results) > 0 {
			// Flat format: record directly in array
			rec := results[0].(map[string]interface{})
			attrs := rec["attributes"].(map[string]interface{})
			urlStr := attrs["url"].(string)
			// Ensure fallback is gone
			assert.Contains(t, urlStr, "/sobjects/Account/<nil>")
		}
	})
}

func TestSearchHandler_WhereClause(t *testing.T) {
	gin.SetMode(gin.TestMode)

	t.Run("Parses WHERE clause in SOSL", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
		c.Set(constants.ContextKeyUser, authUser)

		query := "FIND {laptop} RETURNING Case(Id, Subject, Status WHERE Status='New' AND Priority='High')"
		c.Request = httptest.NewRequest("GET", "/search?q="+url.QueryEscape(query), nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
		handler := rest.NewSearchHandler(svcMgr)

		// Mock Setup with FIELDS
		schemaCase := &models.ObjectMetadata{
			APIName:     "Case",
			PluralLabel: "Cases",
			Searchable:  true,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: constants.FieldTypeString},
				{APIName: "Subject", Type: constants.FieldTypeString, IsNameField: true},
				{APIName: "Status", Type: constants.FieldTypePicklist},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schemaCase}, nil).Maybe()
		mockMetaRepo.On("GetSchemaByAPIName", mock.Anything, "Case").Return(schemaCase, nil).Maybe()

		mockResults := []models.SObject{{"Id": "500xxx", "Subject": "My Laptop"}}
		expectedFilter := "Status='New' AND Priority='High'"

		mockQuery.On("SearchSingleObject",
			mock.Anything,
			"Case",
			"laptop",
			mock.Anything, // search params expanded from scope
			[]string{"Id", "Subject", "Status"},
			500, 0, "", "",
			expectedFilter,
			mock.Anything,
		).Return(mockResults, nil)

		handler.SearchSOSL(c)
		assert.Equal(t, http.StatusOK, w.Code)
		mockQuery.AssertExpectations(t)
	})
}

func TestSearchHandler_SearchParameterized_Where(t *testing.T) {
	gin.SetMode(gin.TestMode)

	t.Run("Passes where param to SearchSingleObject", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
		c.Set(constants.ContextKeyUser, authUser)

		c.Request = httptest.NewRequest("GET", "/parameterizedSearch?q=laptop&sobject=Case&where=Status='New'", nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
		handler := rest.NewSearchHandler(svcMgr)

		// Mock Setup with FIELDS
		schemaCase := &models.ObjectMetadata{
			APIName:     "Case",
			PluralLabel: "Cases",
			Searchable:  true,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: constants.FieldTypeString},
				{APIName: "Subject", Type: constants.FieldTypeString, IsNameField: true},
				{APIName: "Status", Type: constants.FieldTypePicklist},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schemaCase}, nil).Maybe()
		mockMetaRepo.On("GetSchemaByAPIName", mock.Anything, "Case").Return(schemaCase, nil).Maybe()

		mockResults := []models.SObject{{"Id": "500xxx", "Subject": "My Laptop"}}

		mockQuery.On("SearchSingleObject",
			mock.Anything,
			"Case",
			"laptop",
			mock.Anything,
			[]string(nil),
			500, 0, "", "",
			"Status='New'",
			mock.Anything,
		).Return(mockResults, nil)

		handler.SearchParameterized(c)
		assert.Equal(t, http.StatusOK, w.Code)
		mockQuery.AssertExpectations(t)
	})
}

func TestSearchHandler_ExplicitReturningSearchesSystemObjects(t *testing.T) {
	gin.SetMode(gin.TestMode)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
	c.Set(constants.ContextKeyUser, authUser)

	query := "FIND {test} RETURNING UserRole(Id, Name)"
	c.Request = httptest.NewRequest("GET", "/search?q="+url.QueryEscape(query), nil)

	mockQuery := new(MockQueryService)
	mockMetaRepo := new(MockMetadataRepo)
	metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
	permSvc := &permissions.PermissionService{}
	svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
	handler := rest.NewSearchHandler(svcMgr)

	schema := &models.ObjectMetadata{
		APIName:    constants.TableUserRole,
		Searchable: true,
		TableType:  constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString},
			{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}
	mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil).Maybe()
	mockMetaRepo.On("GetSchemaByAPIName", mock.Anything, constants.TableUserRole).Return(schema, nil).Maybe()
	mockQuery.On("SearchSingleObject", mock.Anything, constants.TableUserRole, "test", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
		Return([]models.SObject{}, nil)

	handler.SearchSOSL(c)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	results := resp["searchRecords"].([]interface{})
	assert.Empty(t, results)
	mockQuery.AssertCalled(t, "SearchSingleObject", mock.Anything, constants.TableUserRole, "test", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
}

func TestSearchHandler_SkipsExplicitUnsearchableObjects(t *testing.T) {
	gin.SetMode(gin.TestMode)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
	c.Set(constants.ContextKeyUser, authUser)

	body := `{"q":"test","sobjects":[{"name":"Account","fields":["Id","Name"]}]}`
	c.Request = httptest.NewRequest("POST", "/parameterizedSearch", strings.NewReader(body))
	c.Request.Header.Set("Content-Type", "application/json")

	mockQuery := new(MockQueryService)
	mockMetaRepo := new(MockMetadataRepo)
	metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
	permSvc := &permissions.PermissionService{}
	svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
	handler := rest.NewSearchHandler(svcMgr)

	schema := &models.ObjectMetadata{
		APIName:    "Account",
		Searchable: false,
		TableType:  constants.TableTypeStandardObject,
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString},
			{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}
	mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil).Maybe()
	mockMetaRepo.On("GetSchemaByAPIName", mock.Anything, "Account").Return(schema, nil).Maybe()

	handler.SearchParameterizedPOST(c)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	results := resp["searchRecords"].([]interface{})
	assert.Empty(t, results)
	mockQuery.AssertNotCalled(t, "SearchSingleObject", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
}

func TestSearchHandler_ScopeStrictness(t *testing.T) {
	gin.SetMode(gin.TestMode)

	t.Run("IN NAME FIELDS ignores matches in Email field", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
		c.Set(constants.ContextKeyUser, authUser)

		query := "FIND {bob@example.com} IN NAME FIELDS RETURNING Contact(Id, Email)"
		c.Request = httptest.NewRequest("GET", "/search?q="+url.QueryEscape(query), nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
		handler := rest.NewSearchHandler(svcMgr)

		// Mocks with FIELDS
		schema := &models.ObjectMetadata{
			APIName:     "Contact",
			PluralLabel: "Contacts",
			Searchable:  true,
			Fields: []models.FieldMetadata{
				{APIName: "Name", IsNameField: true, Type: constants.FieldTypeString},
				{APIName: "Email", Type: constants.FieldTypeEmail},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil).Maybe()

		// EXPECTATION: SearchSingleObject should receive searchFields=["Name"], NOT nil due to scope strictness
		mockQuery.On("SearchSingleObject",
			mock.Anything,
			"Contact",
			"bob@example.com",
			[]string{"Name"}, // <-- STRICT SCOPE
			[]string{"Id", "Email"},
			500, 0, "", "", "",
			mock.Anything,
		).Return([]models.SObject{}, nil)

		handler.SearchSOSL(c)
		assert.Equal(t, http.StatusOK, w.Code)
		mockQuery.AssertExpectations(t)
	})

	t.Run("IN EMAIL FIELDS targets Email field", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
		c.Set(constants.ContextKeyUser, authUser)

		query := "FIND {bob@example.com} IN EMAIL FIELDS RETURNING Contact(Id, Email)"
		c.Request = httptest.NewRequest("GET", "/search?q="+url.QueryEscape(query), nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
		handler := rest.NewSearchHandler(svcMgr)

		// Mocks with FIELDS
		schema := &models.ObjectMetadata{
			APIName:     "Contact",
			PluralLabel: "Contacts",
			Searchable:  true,
			Fields: []models.FieldMetadata{
				{APIName: "Name", IsNameField: true, Type: constants.FieldTypeString},
				{APIName: "Email", Type: constants.FieldTypeEmail},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil).Maybe()

		// EXPECTATION: SearchSingleObject should receive searchFields=["Email"]
		mockQuery.On("SearchSingleObject",
			mock.Anything,
			"Contact",
			"bob@example.com",
			[]string{"Email"}, // <-- STRICT SCOPE
			[]string{"Id", "Email"},
			500, 0, "", "", "",
			mock.Anything,
		).Return([]models.SObject{{"Id": "003xxx", "Email": "bob@example.com"}}, nil)

		handler.SearchSOSL(c)
		assert.Equal(t, http.StatusOK, w.Code)
		mockQuery.AssertExpectations(t)
	})

	t.Run("IN ALL FIELDS uses metadata-configured list and name fields", func(t *testing.T) {
		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		authUser := auth.UserSession{ID: "user1", ProfileId: constants.ProfileSystemAdmin}
		c.Set(constants.ContextKeyUser, authUser)

		query := "FIND {codex} IN ALL FIELDS RETURNING Lead(Id, Name, Email)"
		c.Request = httptest.NewRequest("GET", "/search?q="+url.QueryEscape(query), nil)

		mockQuery := new(MockQueryService)
		mockMetaRepo := new(MockMetadataRepo)
		metaSvc := metadata.NewMetadataService(mockMetaRepo, nil, nil)
		permSvc := &permissions.PermissionService{}
		svcMgr := &services.ServiceManager{QuerySvc: mockQuery, Metadata: metaSvc, Permissions: permSvc}
		handler := rest.NewSearchHandler(svcMgr)

		schema := &models.ObjectMetadata{
			APIName:     "Lead",
			PluralLabel: "Leads",
			Searchable:  true,
			ListFields:  []string{"Company", "Email", "Phone"},
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: constants.FieldTypeString},
				{APIName: "Name", IsNameField: true, Type: constants.FieldTypeString},
				{APIName: "FirstName", Type: constants.FieldTypeString},
				{APIName: "LastName", Type: constants.FieldTypeString},
				{APIName: "Company", Type: constants.FieldTypeString},
				{APIName: "Email", Type: constants.FieldTypeEmail},
				{APIName: "Phone", Type: constants.FieldTypePhone},
				{APIName: "Title", Type: constants.FieldTypeString},
				{APIName: "LeadSource", Type: constants.FieldTypePicklist},
				{APIName: "Description", Type: constants.FieldTypeTextarea},
				{APIName: "Website", Type: constants.FieldTypeUrl},
				{APIName: "ExtraA__c", Type: constants.FieldTypeString},
				{APIName: "ExtraB__c", Type: constants.FieldTypeString},
				{APIName: "ExtraC__c", Type: constants.FieldTypeString},
			},
		}
		mockMetaRepo.On("GetAllSchemas", mock.Anything).Return([]*models.ObjectMetadata{schema}, nil).Maybe()

		mockQuery.On("SearchSingleObject",
			mock.Anything,
			"Lead",
			"codex",
			[]string{"Name", "FirstName", "LastName", "Company", "Email", "Phone"},
			[]string{"Id", "Name", "Email"},
			500, 0, "", "", "",
			mock.Anything,
		).Return([]models.SObject{}, nil)

		handler.SearchSOSL(c)
		assert.Equal(t, http.StatusOK, w.Code)
		mockQuery.AssertExpectations(t)
	})
}
func (m *MockMetadataRepo) MarkLayoutCustomized(_ context.Context, _ string) {}
func (m *MockMetadataRepo) MarkFlowDefinitionCustomized(_ context.Context, _ string) error {
	return nil
}

// --- Layout Assignment interface stubs (WP2) ---
func (m *MockMetadataRepo) FindBestAssignment(_ context.Context, _ string, _ []string, _ string, _ string) (*models.LayoutAssignment, error) {
	return nil, nil
}
func (m *MockMetadataRepo) CreateAssignment(_ context.Context, _ *models.LayoutAssignment) error {
	return nil
}
func (m *MockMetadataRepo) DeleteAssignment(_ context.Context, _ string) error { return nil }
func (m *MockMetadataRepo) GetAssignmentsForObject(_ context.Context, _ string) ([]*models.LayoutAssignment, error) {
	return nil, nil
}
func (m *MockMetadataRepo) DeleteAssignmentsByLayoutID(_ context.Context, _ string) error { 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 }

// Approval layout methods
func (m *MockMetadataRepo) GetApprovalLayouts(ctx context.Context, objectAPIName string) ([]*models.ApprovalPageLayout, error) {
	return nil, nil
}
func (m *MockMetadataRepo) GetApprovalLayout(ctx context.Context, id string) (*models.ApprovalPageLayout, error) {
	return nil, nil
}
func (m *MockMetadataRepo) SaveApprovalLayout(ctx context.Context, layout *models.ApprovalPageLayout) error {
	return nil
}
func (m *MockMetadataRepo) DeleteApprovalLayout(ctx context.Context, id string) error {
	return nil
}
func (m *MockMetadataRepo) SetDefaultApprovalLayout(ctx context.Context, objectAPIName string, layoutID string) error {
	return nil
}
