package analytics

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	domainrls "github.com/nexuscrm/kernel/pkg/rls"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

func TestQueryParameterlessCountUsesTotalSizeOnlyShape(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: int64(3)}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"COUNT()"},
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 3 {
		t.Fatalf("TotalSize = %d, want 3", result.TotalSize)
	}
	if len(result.Records) != 0 {
		t.Fatalf("records = %#v, want empty for parameterless COUNT()", result.Records)
	}
	if !result.Done {
		t.Fatal("Done = false, want true")
	}
	if repo.analyticsQuery.Operation != constants.AnalyticsOperationCount {
		t.Fatalf("analytics operation = %q, want count", repo.analyticsQuery.Operation)
	}
	if repo.analyticsQuery.Field != nil {
		t.Fatalf("analytics field = %q, want nil for COUNT()", *repo.analyticsQuery.Field)
	}
}

func TestCountWithSpecUsesOneAggregatePipelineWithoutRowProjection(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: int64(7)}
	qs := countShapeQueryService(repo)

	count, err := qs.CountWithSpec(context.Background(), queryspec.CountSpec{
		ObjectAPIName: countShapeObject,
		Filter:        queryspec.FieldPredicate("Amount__c", ">", 10),
		ScanAll:       true,
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("CountWithSpec: %v", err)
	}
	if count != 7 {
		t.Fatalf("count = %d, want 7", count)
	}
	if repo.analyticsCalls != 1 {
		t.Fatalf("RunAnalytics calls = %d, want exactly 1", repo.analyticsCalls)
	}
	if repo.analyticsQuery.Operation != constants.AnalyticsOperationCount {
		t.Fatalf("analytics operation = %q, want count", repo.analyticsQuery.Operation)
	}
	if !repo.analyticsQuery.ScanAll {
		t.Fatal("ScanAll = false, want true")
	}
	if len(repo.analyticsQuery.Criteria) != 1 || repo.analyticsQuery.Criteria[0].Field != "Amount__c" {
		t.Fatalf("criteria = %#v, want Amount__c filter", repo.analyticsQuery.Criteria)
	}
}

func TestQueryCountFieldKeepsAggregateResultRowShape(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: int64(3)}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"COUNT(Id)"},
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 {
		t.Fatalf("TotalSize = %d, want 1 AggregateResult row", result.TotalSize)
	}
	if len(result.Records) != 1 {
		t.Fatalf("records = %#v, want one AggregateResult row", result.Records)
	}
	if result.Records[0]["expr0"] != int64(3) {
		t.Fatalf("expr0 = %#v, want int64(3)", result.Records[0]["expr0"])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for AggregateResult row")
	}
	if repo.analyticsQuery.Field == nil || *repo.analyticsQuery.Field != constants.FieldID {
		t.Fatalf("analytics field = %#v, want Id for COUNT(Id)", repo.analyticsQuery.Field)
	}
}

func TestQueryCountFieldAliasUsesAliasKey(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: int64(3)}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"COUNT(Id) total"},
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1", result.TotalSize, len(result.Records))
	}
	if result.Records[0]["total"] != int64(3) {
		t.Fatalf("total alias = %#v, want int64(3)", result.Records[0]["total"])
	}
	if _, ok := result.Records[0]["expr0"]; ok {
		t.Fatalf("expr0 leaked for aliased aggregate: %#v", result.Records[0])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for AggregateResult row")
	}
}

func TestQueryParameterlessCountAliasUsesAggregateResultRowShape(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: int64(3)}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"COUNT() total"},
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1 for aliased COUNT()", result.TotalSize, len(result.Records))
	}
	if result.Records[0]["total"] != int64(3) {
		t.Fatalf("total alias = %#v, want int64(3)", result.Records[0]["total"])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for aliased COUNT() row")
	}
}

func TestQueryScalarAggregateAliasUsesAliasKey(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: float64(42)}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"SUM(Amount__c) totalAmount"},
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1", result.TotalSize, len(result.Records))
	}
	if result.Records[0]["totalAmount"] != float64(42) {
		t.Fatalf("totalAmount alias = %#v, want 42", result.Records[0]["totalAmount"])
	}
	if _, ok := result.Records[0]["expr0"]; ok {
		t.Fatalf("expr0 leaked for aliased scalar aggregate: %#v", result.Records[0])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for AggregateResult row")
	}
	if repo.analyticsQuery.Operation != constants.AnalyticsOperationSum {
		t.Fatalf("analytics operation = %q, want sum", repo.analyticsQuery.Operation)
	}
	if repo.analyticsQuery.Field == nil || *repo.analyticsQuery.Field != "Amount__c" {
		t.Fatalf("analytics field = %#v, want Amount__c", repo.analyticsQuery.Field)
	}
}

func TestQueryGroupedParameterlessCountKeepsAggregateResultRows(t *testing.T) {
	repo := &countShapeRepository{
		analyticsResult: []models.SObject{{
			"name": "A",
			"val":  int64(2),
		}},
	}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"COUNT()"},
		GroupBy:       constants.FieldName,
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1", result.TotalSize, len(result.Records))
	}
	if result.Records[0]["expr0"] != int64(2) {
		t.Fatalf("expr0 = %#v, want int64(2)", result.Records[0]["expr0"])
	}
	if result.Records[0][constants.FieldName] != "A" {
		t.Fatalf("group field = %#v, want A", result.Records[0][constants.FieldName])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for grouped AggregateResult rows")
	}
}

func TestQueryGroupedCountAliasUsesAliasKey(t *testing.T) {
	repo := &countShapeRepository{
		analyticsResult: []models.SObject{{
			"name": "A",
			"val":  int64(2),
		}},
	}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"COUNT(Id) total"},
		GroupBy:       constants.FieldName,
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1", result.TotalSize, len(result.Records))
	}
	if result.Records[0]["total"] != int64(2) {
		t.Fatalf("total alias = %#v, want int64(2)", result.Records[0]["total"])
	}
	if _, ok := result.Records[0]["expr0"]; ok {
		t.Fatalf("expr0 leaked for aliased grouped aggregate: %#v", result.Records[0])
	}
	if result.Records[0][constants.FieldName] != "A" {
		t.Fatalf("group field = %#v, want A", result.Records[0][constants.FieldName])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for grouped AggregateResult rows")
	}
}

func TestQueryGroupedMultipleAggregatesReturnsAllAliasKeys(t *testing.T) {
	repo := &countShapeRepository{
		analyticsResult: []models.SObject{{
			"name": "Prospecting",
			"agg0": int64(2),
			"agg1": float64(125.5),
			"agg2": float64(62.75),
			"agg3": float64(50),
			"agg4": float64(75.5),
		}},
	}
	qs := countShapeQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields: []string{
			"COUNT(Id) c",
			"SUM(Amount__c) s",
			"AVG(Amount__c) a",
			"MIN(Amount__c) minAmount",
			"MAX(Amount__c) maxAmount",
		},
		GroupBy: constants.FieldName,
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1", result.TotalSize, len(result.Records))
	}
	row := result.Records[0]
	if row[constants.FieldName] != "Prospecting" {
		t.Fatalf("group field = %#v, want Prospecting", row[constants.FieldName])
	}
	if row["c"] != int64(2) {
		t.Fatalf("COUNT alias c = %#v, want int64(2)", row["c"])
	}
	if row["s"] != float64(125.5) {
		t.Fatalf("SUM alias s = %#v, want 125.5", row["s"])
	}
	if row["a"] != float64(62.75) {
		t.Fatalf("AVG alias a = %#v, want 62.75", row["a"])
	}
	if row["minAmount"] != float64(50) {
		t.Fatalf("MIN alias minAmount = %#v, want 50", row["minAmount"])
	}
	if row["maxAmount"] != float64(75.5) {
		t.Fatalf("MAX alias maxAmount = %#v, want 75.5", row["maxAmount"])
	}
	if _, ok := row["expr0"]; ok {
		t.Fatalf("expr0 leaked for aliased grouped aggregates: %#v", row)
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for grouped AggregateResult rows")
	}
	if len(repo.analyticsQuery.Aggregates) != 5 {
		t.Fatalf("analytics aggregates = %#v, want COUNT/SUM/AVG/MIN/MAX", repo.analyticsQuery.Aggregates)
	}
	if repo.analyticsQuery.Aggregates[0].Type != "COUNT" || repo.analyticsQuery.Aggregates[0].Field != constants.FieldID {
		t.Fatalf("first aggregate = %#v, want COUNT(Id)", repo.analyticsQuery.Aggregates[0])
	}
	if repo.analyticsQuery.Aggregates[1].Type != "SUM" || repo.analyticsQuery.Aggregates[1].Field != "Amount__c" {
		t.Fatalf("second aggregate = %#v, want SUM(Amount__c)", repo.analyticsQuery.Aggregates[1])
	}
	if repo.analyticsQuery.Aggregates[2].Type != "AVG" || repo.analyticsQuery.Aggregates[2].Field != "Amount__c" {
		t.Fatalf("third aggregate = %#v, want AVG(Amount__c)", repo.analyticsQuery.Aggregates[2])
	}
	if repo.analyticsQuery.Aggregates[3].Type != "MIN" || repo.analyticsQuery.Aggregates[3].Field != "Amount__c" {
		t.Fatalf("fourth aggregate = %#v, want MIN(Amount__c)", repo.analyticsQuery.Aggregates[3])
	}
	if repo.analyticsQuery.Aggregates[4].Type != "MAX" || repo.analyticsQuery.Aggregates[4].Field != "Amount__c" {
		t.Fatalf("fifth aggregate = %#v, want MAX(Amount__c)", repo.analyticsQuery.Aggregates[4])
	}
}

func TestQueryOpportunityGroupedCountAndSumUsesStandardFields(t *testing.T) {
	repo := &countShapeRepository{
		analyticsResult: []models.SObject{{
			"name": "Prospecting",
			"agg0": int64(2),
			"agg1": float64(125.5),
		}},
	}
	qs := opportunityAggregateQueryService(repo)

	result, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: "Opportunity",
		SelectFields: []string{
			"StageName",
			"COUNT(Id) c",
			"SUM(Amount) s",
		},
		GroupBy: "StageName",
	}, countShapeAdminUser())
	if err != nil {
		t.Fatalf("Query: %v", err)
	}

	if result.TotalSize != 1 || len(result.Records) != 1 {
		t.Fatalf("result size = totalSize %d len %d, want 1/1", result.TotalSize, len(result.Records))
	}
	row := result.Records[0]
	if row["StageName"] != "Prospecting" {
		t.Fatalf("StageName = %#v, want Prospecting", row["StageName"])
	}
	if row["c"] != int64(2) {
		t.Fatalf("COUNT alias c = %#v, want int64(2)", row["c"])
	}
	if row["s"] != float64(125.5) {
		t.Fatalf("SUM alias s = %#v, want 125.5", row["s"])
	}
	if !result.IsAggregate {
		t.Fatal("IsAggregate = false, want true for grouped AggregateResult rows")
	}
	if len(repo.analyticsQuery.Aggregates) != 2 {
		t.Fatalf("analytics aggregates = %#v, want COUNT/SUM", repo.analyticsQuery.Aggregates)
	}
	if repo.analyticsQuery.Aggregates[0].Type != "COUNT" || repo.analyticsQuery.Aggregates[0].Field != constants.FieldID {
		t.Fatalf("first aggregate = %#v, want COUNT(Id)", repo.analyticsQuery.Aggregates[0])
	}
	if repo.analyticsQuery.Aggregates[1].Type != "SUM" || repo.analyticsQuery.Aggregates[1].Field != "Amount" {
		t.Fatalf("second aggregate = %#v, want SUM(Amount)", repo.analyticsQuery.Aggregates[1])
	}
}

func TestQueryGroupedAggregateRejectsBareNonGroupedSelectField(t *testing.T) {
	repo := &countShapeRepository{analyticsResult: []models.SObject{{"name": "Prospecting", "val": int64(2)}}}
	qs := countShapeQueryService(repo)

	_, err := qs.Query(context.Background(), queryspec.QuerySpec{
		ObjectAPIName: countShapeObject,
		SelectFields:  []string{"Amount__c", "COUNT(Id) c"},
		GroupBy:       constants.FieldName,
	}, countShapeAdminUser())
	if err == nil {
		t.Fatal("Query error = nil, want bare non-grouped field rejection")
	}
	if !strings.Contains(err.Error(), `field "Amount__c" must appear in GROUP BY or be aggregated`) {
		t.Fatalf("Query error = %q, want non-grouped field rejection", err.Error())
	}
	if repo.analyticsQuery.Operation != "" {
		t.Fatalf("RunAnalytics was called with query %#v; want validation before execution", repo.analyticsQuery)
	}
}

func TestRunAnalyticsSkipsReadProtectedLookupGroupNameHydration(t *testing.T) {
	accountRel := "Account"
	junctionSchema := &models.ObjectMetadata{
		APIName:      "Junction__c",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{
				APIName:          "AccountId",
				Type:             constants.FieldTypeReference,
				ReferenceTo:      []string{"Account"},
				RelationshipName: &accountRel,
			},
		},
	}
	accountSchema := &models.ObjectMetadata{
		APIName:      "Account",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{
				APIName:              constants.FieldName,
				Type:                 constants.FieldTypeString,
				IsNameField:          true,
				ReadProtectionPolicy: constants.FieldReadProtectionBlocked,
			},
		},
	}
	metaRepo := &displayNameMetadataRepository{schemas: []*models.ObjectMetadata{junctionSchema, accountSchema}}
	metadataSvc := metadata.NewMetadataService(metaRepo, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{}, metadataSvc, nil)
	repo := &lookupGroupNameRepository{
		analyticsResult: []models.SObject{{
			"name": "acc-1",
			"val":  int64(1),
		}},
	}
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)
	groupBy := "AccountId"
	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "test read-protected analytics group name")

	got, err := qs.RunAnalytics(ctx, models.AnalyticsQuery{
		ObjectAPIName: "Junction__c",
		Operation:     constants.AnalyticsOperationGroupBy,
		GroupBy:       &groupBy,
	}, &models.UserSession{ID: "u1", ProfileID: "p1"})
	if err != nil {
		t.Fatalf("RunAnalytics: %v", err)
	}

	rows, ok := got.([]models.SObject)
	if !ok || len(rows) != 1 {
		t.Fatalf("RunAnalytics result = %#v, want one grouped row", got)
	}
	if rows[0]["name"] != "acc-1" {
		t.Fatalf("group name = %#v, want unresolved lookup id", rows[0]["name"])
	}
	if repo.lookupRecordCalled {
		t.Fatalf("GetRecordsByIDs called despite read-protected display field %q", repo.lookupNameField)
	}
}

func TestRunAnalyticsSkipsUnreadableLookupGroupNameHydration(t *testing.T) {
	accountRel := "Account"
	junctionSchema := &models.ObjectMetadata{
		APIName:      "Junction__c",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{
				APIName:          "AccountId",
				Type:             constants.FieldTypeReference,
				ReferenceTo:      []string{"Account"},
				RelationshipName: &accountRel,
			},
		},
	}
	accountSchema := &models.ObjectMetadata{
		APIName:      "Account",
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{
				APIName:     constants.FieldName,
				Type:        constants.FieldTypeString,
				IsNameField: true,
			},
			{
				APIName:     constants.FieldOwnerID,
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{constants.TableUser},
				IsSystem:    true,
			},
		},
	}
	metaRepo := &displayNameMetadataRepository{schemas: []*models.ObjectMetadata{junctionSchema, accountSchema}}
	metadataSvc := metadata.NewMetadataService(metaRepo, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{}, metadataSvc, nil)
	repo := &lookupGroupNameRepository{
		analyticsResult: []models.SObject{{
			"name": "acc-1",
			"val":  int64(1),
		}},
		lookupRecordsByID: map[string]models.SObject{
			"acc-1": {
				constants.FieldID:      "acc-1",
				constants.FieldName:    "Private Acme",
				constants.FieldOwnerID: "u2",
			},
		},
	}
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)
	groupBy := "AccountId"

	got, err := qs.RunAnalytics(context.Background(), models.AnalyticsQuery{
		ObjectAPIName: "Junction__c",
		Operation:     constants.AnalyticsOperationGroupBy,
		GroupBy:       &groupBy,
	}, &models.UserSession{ID: "u1", ProfileID: "p1"})
	if err != nil {
		t.Fatalf("RunAnalytics: %v", err)
	}

	rows, ok := got.([]models.SObject)
	if !ok || len(rows) != 1 {
		t.Fatalf("RunAnalytics result = %#v, want one grouped row", got)
	}
	if rows[0]["name"] != "acc-1" {
		t.Fatalf("group name = %#v, want unresolved lookup id", rows[0]["name"])
	}
	if rows[0]["val"] != int64(1) {
		t.Fatalf("val = %#v, want 1", rows[0]["val"])
	}
	if !repo.lookupRecordCalled {
		t.Fatalf("GetRecordsByIDs not called for unreadable target lookup")
	}
	if len(repo.lookupRecordIDs) != 1 || repo.lookupRecordIDs[0] != "acc-1" {
		t.Fatalf("lookup record ids = %#v, want [acc-1]", repo.lookupRecordIDs)
	}
}

const countShapeObject = "GenericRecord"

type countShapeRepository struct {
	ports.QueryRepository

	analyticsResult interface{}
	analyticsQuery  models.AnalyticsQuery
	analyticsCalls  int
}

func (r *countShapeRepository) RunAnalytics(ctx context.Context, tableSchema *models.ObjectMetadata, q models.AnalyticsQuery, rlsResult *domainrls.Result) (interface{}, error) {
	r.analyticsCalls++
	r.analyticsQuery = q
	return r.analyticsResult, nil
}

func countShapeQueryService(repo *countShapeRepository) *QueryService {
	schema := &models.ObjectMetadata{
		APIName:      countShapeObject,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Amount__c", Type: constants.FieldTypeDouble},
		},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{}, metadataSvc, nil)
	return NewQueryService(repo, metadataSvc, permissionSvc, nil)
}

func opportunityAggregateQueryService(repo *countShapeRepository) *QueryService {
	schema := &models.ObjectMetadata{
		APIName:      "Opportunity",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "StageName", Type: constants.FieldTypePicklist},
			{APIName: "Amount", Type: constants.FieldTypeCurrency},
		},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{}, metadataSvc, nil)
	return NewQueryService(repo, metadataSvc, permissionSvc, nil)
}

func countShapeAdminUser() *models.UserSession {
	return &models.UserSession{ID: "005-count", ProfileID: "00e-admin", IsSystemAdmin: true}
}

type lookupGroupNameRepository struct {
	ports.QueryRepository

	analyticsResult interface{}
	lookupCalled    bool
	lookupNameField string

	lookupRecordCalled bool
	lookupRecordIDs    []string
	lookupRecordsByID  map[string]models.SObject
}

func (r *lookupGroupNameRepository) RunAnalytics(ctx context.Context, tableSchema *models.ObjectMetadata, q models.AnalyticsQuery, rlsResult *domainrls.Result) (interface{}, error) {
	return r.analyticsResult, nil
}

func (r *lookupGroupNameRepository) GetLookupNames(ctx context.Context, tableSchema *models.ObjectMetadata, ids []string, idField string, nameField string) ([]models.SObject, error) {
	r.lookupCalled = true
	r.lookupNameField = nameField
	return []models.SObject{{
		idField:   "acc-1",
		nameField: "Acme",
	}}, nil
}

func (r *lookupGroupNameRepository) GetRecordsByIDs(ctx context.Context, tableSchema *models.ObjectMetadata, ids []string, fields []string) ([]models.SObject, error) {
	r.lookupRecordCalled = true
	r.lookupRecordIDs = append([]string(nil), ids...)
	if r.lookupRecordsByID == nil {
		return nil, nil
	}
	results := make([]models.SObject, 0, len(ids))
	for _, id := range ids {
		if rec, ok := r.lookupRecordsByID[id]; ok {
			results = append(results, projectDisplayNameRecord(rec, fields))
		}
	}
	return results, nil
}
