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"
)

func TestSearchSingleObjectProjectsVirtualCompoundAndPrunesDeniedSupportFields(t *testing.T) {
	schema := userVirtualCompoundSchema()
	schema.SharingModel = constants.SharingModelPublicReadWrite
	schema.Fields[0].IsSystem = true

	repo := &capturingSearchRepository{
		results: []models.SObject{{
			constants.FieldID:        "005xx000001",
			constants.FieldFirstName: " Ada ",
			constants.FieldLastName:  " Lovelace ",
		}},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{
			"user.firstname": false,
			"user.lastname":  false,
		},
	}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	got, err := qs.SearchSingleObject(
		context.Background(),
		constants.TableUser,
		"ada",
		[]string{constants.FieldName},
		[]string{constants.FieldName, constants.FieldFirstName},
		10,
		0,
		"",
		"",
		"",
		&models.UserSession{ID: "u1", ProfileID: "p1"},
	)
	if err != nil {
		t.Fatalf("SearchSingleObject: %v", err)
	}

	if !equalAnalyticsStringSlices(repo.searchFields, []string{constants.FieldName}) {
		t.Fatalf("search fields = %#v, want virtual Name only", repo.searchFields)
	}
	if !equalAnalyticsStringSlices(repo.selectFields, []string{constants.FieldID, constants.FieldFirstName, constants.FieldLastName}) {
		t.Fatalf("select fields = %#v, want support fields for synthesis", repo.selectFields)
	}
	if len(got) != 1 {
		t.Fatalf("records = %d, want 1", len(got))
	}
	if got[0][constants.FieldName] != "Ada Lovelace" {
		t.Fatalf("Name = %#v, want Ada Lovelace", got[0][constants.FieldName])
	}
	if _, ok := got[0][constants.FieldFirstName]; ok {
		t.Fatal("FirstName leaked despite denied explicit request")
	}
	if _, ok := got[0][constants.FieldLastName]; ok {
		t.Fatal("LastName leaked as support-only field")
	}
}

// CI HIGH (#1597): a schema whose display name is a LITERAL "Name" column with
// IsNameField=false (a platform-supported shape — see schema_defaults.go: a
// pre-existing "Name" column suppresses the default IsNameField injection).
// Suggestions match on that "Name" column (via suggestionNameSearchFields'
// "Name"-literal priority) and MUST also PROJECT it. The query layer's default
// projection (returnFields=nil) only selects the first IsNameField and has no
// "Name"-literal fallback, so it would select only Id; normalizeNameField then
// falls back to Name == Id ("match A, show B"). The handler now threads the
// matched name field as returnFields, so projection selects the SAME "Name"
// column it matched on — the record carries the real Name value, not the Id.
func TestSearchSuggestionsProjectsLiteralNameFieldWithoutIsNameField(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      "Foo__c",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			// Literal "Name" column, IsNameField deliberately FALSE — the edge shape.
			{APIName: constants.FieldName, Type: constants.FieldTypeString},
		},
	}

	repo := &capturingSearchRepository{
		results: []models.SObject{{
			constants.FieldID:   "foo-1",
			constants.FieldName: "Acme Widget",
		}},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{"foo__c.name": true},
	}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	// The handler passes the matched name field (suggestionNameSearchFields) as
	// BOTH searchFields and returnFields — replicate that here.
	matched := []string{constants.FieldName}
	got, err := qs.SearchSuggestions(
		context.Background(),
		"Foo__c",
		"ac",
		matched,
		matched,
		5,
		&models.UserSession{ID: "u1", ProfileID: "p1"},
	)
	if err != nil {
		t.Fatalf("SearchSuggestions: %v", err)
	}

	// Projection MUST include the literal Name column, not just Id.
	if !equalAnalyticsStringSlices(repo.selectFields, []string{constants.FieldID, constants.FieldName}) {
		t.Fatalf("select fields = %#v, want [Id Name] (projection must match the matched name field)", repo.selectFields)
	}
	if len(got) != 1 {
		t.Fatalf("records = %d, want 1", len(got))
	}
	// The record carries the REAL Name value — not the Id (the pre-fix fallback).
	if got[0][constants.FieldName] != "Acme Widget" {
		t.Fatalf("Name = %#v, want \"Acme Widget\" (the real Name, not the Id)", got[0][constants.FieldName])
	}
	if got[0][constants.FieldName] == got[0][constants.FieldID] {
		t.Fatalf("Name == Id (%#v): projection fell back to Id — the #1597 bug", got[0][constants.FieldName])
	}
}

type capturingSearchRepository struct {
	ports.QueryRepository

	searchFields []string
	selectFields []string
	results      []models.SObject
}

func (r *capturingSearchRepository) Search(ctx context.Context, schema *models.ObjectMetadata, term string, searchFields []string, selectFields []string, limit int, offset int, orderBy string, orderDir string, filterExpr string, rlsResult *domainrls.Result, opts ports.SearchOptions) ([]models.SObject, error) {
	r.searchFields = append([]string(nil), searchFields...)
	r.selectFields = append([]string(nil), selectFields...)
	return r.results, nil
}

type searchMetadataRepository struct {
	ports.MetadataRepository

	schema *models.ObjectMetadata
}

func (r *searchMetadataRepository) GetAllSchemas(ctx context.Context) ([]*models.ObjectMetadata, error) {
	return []*models.ObjectMetadata{r.schema}, nil
}

func (r *searchMetadataRepository) GetSchemaByAPIName(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	if strings.EqualFold(apiName, r.schema.APIName) {
		return r.schema, nil
	}
	return nil, nil
}

func (r *searchMetadataRepository) GetAllRelationships(ctx context.Context) ([]*models.Relationship, error) {
	return nil, nil
}

type searchPermissionRepository struct {
	ports.PermissionRepository

	objectReads          map[string]bool
	fieldReads           map[string]bool
	userGroupIDs         map[string][]string
	groupMemberIDs       map[string][]string
	manualShareRecordIDs map[string][]string
	teamMemberRecordIDs  map[string][]string
	systemPerms          map[string]bool
}

func (r *searchPermissionRepository) GetAllRoles(ctx context.Context) ([]*models.UserRole, error) {
	return nil, nil
}

func (r *searchPermissionRepository) LoadEffectiveObjectPermission(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
	if r.objectReads != nil {
		return &models.ObjectPermissions{PermissionsRead: r.objectReads[strings.ToLower(objectAPIName)]}, nil
	}
	return &models.ObjectPermissions{PermissionsRead: true}, nil
}

func (r *searchPermissionRepository) LoadEffectiveFieldPermission(ctx context.Context, user *models.UserSession, objectAPIName, fieldAPIName string) (*models.FieldPermissions, error) {
	if r.fieldReads == nil {
		return nil, nil
	}
	allowed, ok := r.fieldReads[strings.ToLower(objectAPIName+"."+fieldAPIName)]
	if !ok {
		return nil, nil
	}
	return &models.FieldPermissions{PermissionsRead: allowed}, nil
}

func (r *searchPermissionRepository) GetUserGroupIDs(ctx context.Context, userID string) ([]string, error) {
	if r.userGroupIDs != nil {
		return append([]string(nil), r.userGroupIDs[userID]...), nil
	}
	return nil, nil
}

func (r *searchPermissionRepository) GetManualShareRecordIDs(ctx context.Context, objectAPIName, userID string) ([]string, error) {
	if r.manualShareRecordIDs != nil {
		return append([]string(nil), r.manualShareRecordIDs[strings.ToLower(objectAPIName)]...), nil
	}
	return nil, nil
}

func (r *searchPermissionRepository) GetTeamMemberRecordIDs(ctx context.Context, objectAPIName, userID string) ([]string, error) {
	if r.teamMemberRecordIDs != nil {
		return append([]string(nil), r.teamMemberRecordIDs[strings.ToLower(objectAPIName)]...), nil
	}
	return nil, nil
}

func (r *searchPermissionRepository) GetEffectiveSystemPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	if r.systemPerms == nil {
		return map[string]bool{}, nil
	}
	out := make(map[string]bool, len(r.systemPerms))
	for key, value := range r.systemPerms {
		out[key] = value
	}
	return out, nil
}

func TestSearchSingleObjectStripsFLSDeniedOrdinarySearchField(t *testing.T) {
	schema := &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},
			{APIName: "Email", Type: constants.FieldTypeString},
		},
	}

	repo := &capturingSearchRepository{
		results: []models.SObject{{
			constants.FieldID:   "001xx000001",
			constants.FieldName: "Acme",
		}},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{
			"account.name":  true,
			"account.email": false, // explicitly denied
		},
	}, metadataSvc, nil)
	qs := NewQueryService(repo, metadataSvc, permissionSvc, nil)

	got, err := qs.SearchSingleObject(
		context.Background(),
		"Account",
		"acme",
		[]string{constants.FieldName, "Email"},
		[]string{constants.FieldID, constants.FieldName},
		10,
		0,
		"",
		"",
		"",
		&models.UserSession{ID: "u1", ProfileID: "p1"},
	)
	if err != nil {
		t.Fatalf("SearchSingleObject: %v", err)
	}

	if !equalAnalyticsStringSlices(repo.searchFields, []string{constants.FieldName}) {
		t.Fatalf("search fields = %#v, want [Name] — Email must be stripped by FLS", repo.searchFields)
	}
	if len(got) != 1 {
		t.Fatalf("records = %d, want 1", len(got))
	}
}

func TestProjectPreviewSyntheticRecordsAppliesDefaultQueryProjection(t *testing.T) {
	schema := &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},
			{APIName: "Secret__c", Type: constants.FieldTypeString},
		},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&searchPermissionRepository{
		fieldReads: map[string]bool{
			"account.name":      true,
			"account.secret__c": false,
		},
	}, metadataSvc, nil)
	qs := NewQueryService(&capturingSearchRepository{}, metadataSvc, permissionSvc, nil)

	got, err := qs.ProjectPreviewSyntheticRecords(context.Background(), "Account", []models.SObject{{
		constants.FieldID:   "preview-parent",
		constants.FieldName: "Preview Account",
		"Secret__c":         "hidden",
	}}, &models.UserSession{ID: "u1", ProfileID: "p1"})
	if err != nil {
		t.Fatalf("ProjectPreviewSyntheticRecords: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("records = %d, want 1", len(got))
	}
	if got[0][constants.FieldID] != "preview-parent" {
		t.Fatalf("Id = %#v, want preview-parent", got[0][constants.FieldID])
	}
	if got[0][constants.FieldName] != "Preview Account" {
		t.Fatalf("Name = %#v, want Preview Account", got[0][constants.FieldName])
	}
	if _, ok := got[0]["Secret__c"]; ok {
		t.Fatal("Secret__c leaked from synthetic projection despite denied FLS")
	}
}

func (r *searchPermissionRepository) GetGroupMemberIDs(ctx context.Context, groupIDs []string) ([]string, error) {
	if r.groupMemberIDs != nil {
		members := make([]string, 0)
		for _, groupID := range groupIDs {
			members = append(members, r.groupMemberIDs[groupID]...)
		}
		return members, nil
	}
	return nil, nil
}

func (r *searchPermissionRepository) IsUserInGroup(ctx context.Context, groupID, userID string) (bool, error) {
	if r.userGroupIDs != nil {
		for _, candidate := range r.userGroupIDs[userID] {
			if candidate == groupID {
				return true, nil
			}
		}
	}
	return false, nil
}

func (*searchPermissionRepository) GetManualShareAccessLevels(context.Context, string, string, string) ([]string, error) {
	return nil, nil
}

func (*searchPermissionRepository) GetTeamMemberAccessLevel(context.Context, string, string, string) (*string, error) {
	return nil, nil
}
