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

const scopedFLSObject = "IndexedObject"

func TestQueryWithSpec_ScopedReadEnforceFLSOmitsHiddenNonNameField(t *testing.T) {
	repo := &scopedFLSQueryRepository{}
	qs := scopedFLSQueryService(repo)
	ctx := constants.WithStandardActionInternalAccessScopes(context.Background(), "recycle-bin-read", []constants.StandardActionInternalAccessScope{{
		StandardActionID: "recycle-bin-read",
		ObjectAPIName:    scopedFLSObject,
		Operation:        constants.PermRead,
		EnforceFLS:       true,
	}})

	result, err := qs.QueryWithSpec(ctx, queryspec.QuerySpec{ObjectAPIName: scopedFLSObject}, scopedFLSUser())
	if err != nil {
		t.Fatalf("QueryWithSpec: %v", err)
	}
	if !repo.findCalled {
		t.Fatal("repo.Find was not called; scoped read did not bypass object-read denial")
	}
	if !repo.rlsWasNil {
		t.Fatal("repo.Find received an RLS result; scoped read did not bypass RLS")
	}
	if containsAnalyticsField(repo.selectFields, "HiddenText") {
		t.Fatalf("hidden non-name field was selected under EnforceFLS=true: %v", repo.selectFields)
	}
	if !containsAnalyticsField(repo.selectFields, "VisibleText") {
		t.Fatalf("visible field was not selected under EnforceFLS=true: %v", repo.selectFields)
	}
	if len(result.Records) != 1 {
		t.Fatalf("records = %d, want 1", len(result.Records))
	}
	if _, ok := result.Records[0]["HiddenText"]; ok {
		t.Fatalf("hidden non-name field leaked in result: %#v", result.Records[0])
	}
	if result.Records[0]["VisibleText"] != "shown" {
		t.Fatalf("VisibleText = %#v, want shown", result.Records[0]["VisibleText"])
	}
}

func TestQueryWithSpec_ScopedReadDefaultStillBypassesFLS(t *testing.T) {
	repo := &scopedFLSQueryRepository{}
	qs := scopedFLSQueryService(repo)
	ctx := constants.WithStandardActionInternalAccessScopes(context.Background(), "standard-action-read", []constants.StandardActionInternalAccessScope{{
		StandardActionID: "standard-action-read",
		ObjectAPIName:    scopedFLSObject,
		Operation:        constants.PermRead,
	}})

	result, err := qs.QueryWithSpec(ctx, queryspec.QuerySpec{ObjectAPIName: scopedFLSObject}, scopedFLSUser())
	if err != nil {
		t.Fatalf("QueryWithSpec: %v", err)
	}
	if !repo.findCalled {
		t.Fatal("repo.Find was not called; scoped read did not bypass object-read denial")
	}
	if !repo.rlsWasNil {
		t.Fatal("repo.Find received an RLS result; scoped read did not bypass RLS")
	}
	if !containsAnalyticsField(repo.selectFields, "HiddenText") {
		t.Fatalf("legacy scoped read no longer selected hidden field: %v", repo.selectFields)
	}
	if len(result.Records) != 1 {
		t.Fatalf("records = %d, want 1", len(result.Records))
	}
	if result.Records[0]["HiddenText"] != "hidden" {
		t.Fatalf("HiddenText = %#v, want hidden for legacy scoped read", result.Records[0]["HiddenText"])
	}
}

func scopedFLSQueryService(repo *scopedFLSQueryRepository) *QueryService {
	schema := &models.ObjectMetadata{
		APIName:      scopedFLSObject,
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "VisibleText", Type: constants.FieldTypeString},
			{APIName: "HiddenText", Type: constants.FieldTypeString},
		},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&scopedFLSPermissionRepository{
		objectReads: map[string]bool{
			strings.ToLower(scopedFLSObject): false,
		},
		fieldReads: map[string]bool{
			strings.ToLower(scopedFLSObject + "." + constants.FieldName): true,
			strings.ToLower(scopedFLSObject + ".VisibleText"):            true,
			strings.ToLower(scopedFLSObject + ".HiddenText"):             false,
		},
	}, metadataSvc, nil)
	return NewQueryService(repo, metadataSvc, permissionSvc, nil)
}

func scopedFLSUser() *models.UserSession {
	return &models.UserSession{ID: "005-reader", ProfileID: "00e-standard"}
}

type scopedFLSQueryRepository struct {
	ports.QueryRepository

	findCalled   bool
	selectFields []string
	rlsWasNil    bool
}

func (r *scopedFLSQueryRepository) Find(ctx context.Context, schema *models.ObjectMetadata, req models.QueryRequest, rlsResult *domainrls.Result, visibleFields []string) (*models.QueryResult, error) {
	r.findCalled = true
	r.selectFields = append([]string(nil), req.SelectFields...)
	r.rlsWasNil = rlsResult == nil

	values := map[string]interface{}{
		constants.FieldID:   "idx-1",
		constants.FieldName: "Indexed",
		"VisibleText":       "shown",
		"HiddenText":        "hidden",
	}
	row := models.SObject{}
	for _, field := range req.SelectFields {
		if value, ok := values[field]; ok {
			row[field] = value
		}
	}
	return &models.QueryResult{Records: []models.SObject{row}, TotalSize: 1, Done: true}, nil
}

type scopedFLSPermissionRepository struct {
	ports.PermissionRepository

	objectReads map[string]bool
	fieldReads  map[string]bool
}

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

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

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

func (r *scopedFLSPermissionRepository) GetEffectiveSystemPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	return map[string]bool{}, nil
}

func containsAnalyticsField(fields []string, want string) bool {
	for _, field := range fields {
		if strings.EqualFold(field, want) {
			return true
		}
	}
	return false
}

func (*scopedFLSPermissionRepository) GetManualShareRecordIDs(context.Context, string, string) ([]string, error) {
	return nil, nil
}
func (*scopedFLSPermissionRepository) GetTeamMemberRecordIDs(context.Context, string, string) ([]string, error) {
	return nil, nil
}
func (*scopedFLSPermissionRepository) GetGroupMemberIDs(context.Context, []string) ([]string, error) {
	return nil, nil
}

// TestQueryWithSpec_ScopedReadFieldProjectionWhenActorIsDeniedObjectRead pins
// the property established by runtime probe while diagnosing NX#4397: when the
// acting user is denied object read, a scoped read that still enforces the
// actor's field visibility returns the row with every business field stripped,
// because CheckFieldVisibilityWithUser falls back to that same denied
// object-read for any field without an explicit FLS row.
//
// That is the production failure: the approval notification snapshot came back
// as an Id-only row, the notification Flow's `$Record.<field> IsNull` guard
// matched, and the delivery died silently. Bypassing object-read and RLS alone
// does not repair it — the field projection has to stop consulting the actor.
func TestQueryWithSpec_ScopedReadFieldProjectionWhenActorIsDeniedObjectRead(t *testing.T) {
	scopedFor := func(enforceFLS bool) context.Context {
		return constants.WithStandardActionInternalAccessScopes(context.Background(), "engine-read", []constants.StandardActionInternalAccessScope{{
			StandardActionID: "engine-read",
			ObjectAPIName:    scopedFLSDeniedObject,
			Operation:        constants.PermRead,
			EnforceFLS:       enforceFLS,
		}})
	}

	t.Run("EnforceFLS=true strips every business field", func(t *testing.T) {
		repo := &scopedFLSQueryRepository{}
		qs := scopedFLSDeniedQueryService(repo)
		if _, err := qs.QueryWithSpec(scopedFor(true), queryspec.QuerySpec{ObjectAPIName: scopedFLSDeniedObject}, scopedFLSUser()); err != nil {
			t.Fatalf("QueryWithSpec: %v", err)
		}
		if !repo.findCalled {
			t.Fatal("scoped read did not bypass the object-read denial")
		}
		if containsAnalyticsField(repo.selectFields, "VisibleText") {
			t.Fatalf("expected the actor projection to strip business fields, got %v", repo.selectFields)
		}
	})

	t.Run("ignoring implicit denials keeps no-row fields and still hides explicit denials", func(t *testing.T) {
		repo := &scopedFLSQueryRepository{}
		qs := scopedFLSDeniedQueryService(repo)
		ctx := constants.WithStandardActionInternalAccessScopes(context.Background(), "engine-read", []constants.StandardActionInternalAccessScope{{
			StandardActionID:           "engine-read",
			ObjectAPIName:              scopedFLSDeniedObject,
			Operation:                  constants.PermRead,
			EnforceFLS:                 true,
			IgnoreImplicitFieldDenials: true,
		}})
		if _, err := qs.QueryWithSpec(ctx, queryspec.QuerySpec{ObjectAPIName: scopedFLSDeniedObject}, scopedFLSUser()); err != nil {
			t.Fatalf("QueryWithSpec: %v", err)
		}
		// No FieldPermissions row exists for this field: the fallback to the
		// actor's denied object read must not hide it from the engine's read.
		if !containsAnalyticsField(repo.selectFields, "VisibleText") {
			t.Fatalf("a field with no FLS row must survive for an actor denied object read, got %v", repo.selectFields)
		}
		// An explicit PermissionsRead=false row remains authoritative.
		if containsAnalyticsField(repo.selectFields, "DeniedText") {
			t.Fatalf("an explicitly denied field must never reach the snapshot, got %v", repo.selectFields)
		}
	})

	t.Run("without a scope the read is denied outright", func(t *testing.T) {
		repo := &scopedFLSQueryRepository{}
		qs := scopedFLSDeniedQueryService(repo)
		_, err := qs.QueryWithSpec(context.Background(), queryspec.QuerySpec{ObjectAPIName: scopedFLSDeniedObject}, scopedFLSUser())
		if err == nil {
			t.Fatal("expected a permission error for a plain actor read; this is the pre-fix production path that emptied the snapshot")
		}
		if repo.findCalled {
			t.Fatal("query ran despite the object-read denial")
		}
	})
}

const scopedFLSDeniedObject = "DeniedObject"

// scopedFLSDeniedQueryService builds a service where the acting user is denied
// object read and holds no FieldPermissions rows at all — the shape a
// ControlledByParent child presents when its sharing parent cannot be resolved.
func scopedFLSDeniedQueryService(repo *scopedFLSQueryRepository) *QueryService {
	schema := &models.ObjectMetadata{
		APIName:      scopedFLSDeniedObject,
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "VisibleText", Type: constants.FieldTypeString},
			{APIName: "DeniedText", Type: constants.FieldTypeString},
		},
	}
	metadataSvc := metadata.NewMetadataService(&searchMetadataRepository{schema: schema}, nil, nil)
	permissionSvc := permissions.NewPermissionService(&scopedFLSPermissionRepository{
		objectReads: map[string]bool{strings.ToLower(scopedFLSDeniedObject): false},
		// VisibleText has no row at all; DeniedText is explicitly denied.
		fieldReads: map[string]bool{
			strings.ToLower(scopedFLSDeniedObject + ".DeniedText"): false,
		},
	}, metadataSvc, nil)
	return NewQueryService(repo, metadataSvc, permissionSvc, nil)
}
