package analytics

import (
	"context"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
)

// TestScopedReadFieldVisibilityGates pins the platform property the approval
// notification snapshot depends on (NX#4397 forensics): a scoped internal
// access entry decides whether the query path projects fields as the acting
// user or as the engine.
//
// Why this matters beyond the helpers themselves: when a user has no
// FieldPermissions row for a field, PermissionService.CheckFieldVisibilityWithUser
// falls back to object-level read. A read that already bypasses object-read for
// ROWS must not inherit that fallback for FIELDS — an actor denied object read
// then loses every business field, so the row comes back holding Id alone. That
// is the shape production forensics measured for NX#4397, and it fails
// silently: the notification Flow's guard on a now-absent field matches, the
// interview finishes, nothing is sent, and a non-nil snapshot means the
// fail-closed delivery gate never fires either.
//
// The remedy is NOT EnforceFLS=false. That discards EXPLICIT
// PermissionsRead=false rows along with the implicit ones and can carry denied
// values into a notification. A read that must not depend on the actor's object
// grants declares EnforceFLS=true PLUS IgnoreImplicitFieldDenials, which drops
// only the no-row fallback; see approvalNotificationSnapshotReadContext, and
// TestQueryWithSpec_ScopedReadFieldProjectionWhenActorIsDeniedObjectRead, which
// pins that axis against the real query path. This test covers the EnforceFLS
// axis of the same gate only.
func TestScopedReadFieldVisibilityGates(t *testing.T) {
	const objectName = "GenericChild__c"
	const actionID = "test.snapshot-read"

	scoped := func(enforceFLS bool) context.Context {
		ctx := constants.WithSecurityContext(context.Background(), constants.UserContext, "test")
		return constants.WithStandardActionInternalAccessScopes(ctx, actionID, []constants.StandardActionInternalAccessScope{{
			StandardActionID: actionID,
			ObjectAPIName:    objectName,
			Operation:        constants.PermRead,
			EnforceFLS:       enforceFLS,
		}})
	}

	t.Run("EnforceFLS=false projects fields as the engine", func(t *testing.T) {
		ctx := scoped(false)
		if !scopedReadBypassesUserAccess(ctx, objectName) {
			t.Fatal("scoped read does not bypass object-read/RLS")
		}
		if !scopedReadBypassesFieldVisibility(ctx, objectName) {
			t.Fatal("EnforceFLS=false must bypass field visibility outright; the narrow alternative that keeps explicit denials is EnforceFLS=true plus IgnoreImplicitFieldDenials")
		}
		if scopedReadEnforcesFieldVisibility(ctx, objectName) {
			t.Fatal("EnforceFLS=false must not enforce actor field visibility")
		}
	})

	t.Run("EnforceFLS=true keeps the actor field projection", func(t *testing.T) {
		ctx := scoped(true)
		if !scopedReadBypassesUserAccess(ctx, objectName) {
			t.Fatal("scoped read does not bypass object-read/RLS")
		}
		if scopedReadBypassesFieldVisibility(ctx, objectName) {
			t.Fatal("EnforceFLS=true must not bypass field visibility")
		}
		if !scopedReadEnforcesFieldVisibility(ctx, objectName) {
			t.Fatal("EnforceFLS=true must enforce actor field visibility")
		}
	})

	t.Run("the gates are scoped to the declared object", func(t *testing.T) {
		ctx := scoped(false)
		if scopedReadBypassesUserAccess(ctx, "OtherObject__c") {
			t.Fatal("a scope declared for one object must not bypass access on another")
		}
		if scopedReadBypassesFieldVisibility(ctx, "OtherObject__c") {
			t.Fatal("a scope declared for one object must not bypass field visibility on another")
		}
	})
}
