package automation

import (
	"context"
	"testing"

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

// TestApprovalNotificationSnapshotReadIgnoresActorFieldGrants reproduces the
// shape of the production incident behind NX#4397 rather than the shape this
// issue's narrative described, because the two differ and the difference is
// why a green test suite shipped a fix that did not repair the incident.
//
// Established by read-only production forensics:
//
//   - the record under approval lives on a ControlledByParent / master-detail
//     child object;
//   - the acting approver held no explicit FieldPermissions row for the field
//     the notification Flow reads;
//   - the Flow ran exactly one element — its opening guard on that field — and
//     finished, leaving no notification row and no delivery attempt.
//
// Established by runtime probe, which is what settled the mechanism after two
// static readings of the code reached wrong answers:
//
//   - CheckFieldVisibilityWithUser falls back to object-level read when no FLS
//     row exists, so an actor denied object read loses EVERY business field
//     while the row itself is still returned. The snapshot comes back holding
//     Id alone, and a guard on a now-absent field matches exactly as observed.
//
// Deliberately NOT asserted here: why that actor was denied object read on that
// child. An earlier explanation — that a ControlledByParent child carries no
// ObjectPermissions rows, so the fallback can never be satisfied — was
// retracted, because CheckObjectPermissionWithUser recurses into the parent for
// such children. The open lead is tracked in NX#4397.
//
// Bypassing object-read and RLS alone does not fix this: the row comes back
// with its fields stripped and the Flow behaves identically. The snapshot read
// must also declare that it does not inherit the actor's implicit field denials.
func TestApprovalNotificationSnapshotReadIgnoresActorFieldGrants(t *testing.T) {
	const objectName = "GenericChild__c"

	// Build the input the way the approval transaction actually presents it:
	// elevated to SystemContext, carrying a protected-flow stamp and a foreign
	// internal-access scope. A bare context.Background() would let the
	// constructor's stripping and lowering steps be deleted without any
	// assertion below noticing.
	ambient := constants.WithSecurityContext(context.Background(), constants.SystemContext, "approval transaction")
	ambient = constants.WithStandardActionProtectedFlowExecution(ambient, "SomeProtectedRootFlow")
	ambient = constants.WithStandardActionInternalAccessScopes(ambient, "some-other-action", []constants.StandardActionInternalAccessScope{{
		StandardActionID: "some-other-action",
		ObjectAPIName:    "SomeOtherObject__c",
		Operation:        constants.PermRead,
		EnforceFLS:       true,
	}})

	ctx := approvalNotificationSnapshotReadContext(ambient, objectName)

	if !constants.IsScopedInternalAccess(ctx, objectName, constants.PermRead) {
		t.Fatal("snapshot read must declare a scoped internal-access read so object-read and RLS do not gate it")
	}

	// The load-bearing pair. Field-level security stays ON — an explicit
	// PermissionsRead=false row must still hide a field from a notification —
	// while the implicit denial, the fallback to the actor's object-level read
	// that this scope has already been granted an exception to, is ignored.
	// Dropping either half reintroduces a failure: without EnforceFLS the
	// snapshot would carry explicitly denied fields; without the narrowing the
	// incident reproduces unchanged (the probe measured such a read selecting
	// `[Id]` alone).
	if !constants.ScopedInternalAccessEnforcesFLS(ctx, objectName, constants.PermRead) {
		t.Fatal("snapshot read must keep field-level security on so explicitly denied fields stay out of notifications")
	}
	if !constants.ScopedInternalAccessIgnoresImplicitFieldDenials(ctx, objectName, constants.PermRead) {
		t.Fatal("snapshot read still inherits the implicit field denial: a field with no FLS row falls back to the actor's object-level read, and an actor denied object read then loses every field, so the notification payload comes back stripped and the delivery fails silently")
	}

	// Security properties that must survive the widening.
	if constants.GetSecurityContext(ctx) >= constants.SystemContext {
		t.Fatalf("snapshot read must stay below SystemContext, got %v", constants.GetSecurityContext(ctx))
	}
	if constants.HasStandardActionProtectedFlowExecution(ctx) {
		t.Fatal("snapshot read must not inherit the protected standard-action Flow execution scope")
	}
	if constants.IsScopedInternalAccess(ctx, "SomeOtherObject__c", constants.PermRead) {
		t.Fatal("the scope must be confined to the approval target object")
	}
}
