package permissions

import (
	"context"
	"errors"
	"testing"

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

// rlsPolicyMatrixGoldenPath is the checked-in PR-D0a regression snapshot. It pins
// the CURRENT behavior of the RLS taxonomy (BuildRLSResult flat fields + rendered
// SQL) and its single-record twin (CheckRecordAccess / CheckRecordAccessAll) across
// the SharingModel x principal grid, so the upcoming L0-D policy/FLS/RLS binding
// migration cannot silently change a single decision. It adds no evaluator and
// touches no production code; coherence comes purely from the fake repo/metadata
// data below, reusing the existing access-decision golden fixtures.
//
// Regenerate:  UPDATE_GOLDEN=1 GOWORK=off go test ./... -run RLSPolicyMatrixGolden
const rlsPolicyMatrixGoldenPath = "testdata/rls_policy_matrix_golden.json"

// --- Golden DTOs (stable, deterministic JSON shape) --------------------------

type rlsPolicyMatrix struct {
	// Section 1: RLS taxonomy — flat RLSResult fields + rendered SQL per cell.
	Taxonomy []rlsMatrixCell `json:"taxonomy"`
	// Section 2: single-record twin — CheckRecordAccess (per op) + CheckRecordAccessAll.
	RecordTwin []rlsRecordTwinCell `json:"recordTwin"`
}

// rlsMatrixCell snapshots ONLY the flat, deterministic fields of an RLSResult
// plus PRESENCE booleans for the non-flat pointer fields. Pointer fields are
// never serialized (non-deterministic addresses / nested non-flat shape).
type rlsMatrixCell struct {
	SharingModel string `json:"sharingModel"`
	Principal    string `json:"principal"`
	Object       string `json:"object"`

	Skip          bool `json:"skip"`
	DenyAll       bool `json:"denyAll"`
	HasOwnerField bool `json:"hasOwnerField"`

	ObjectAPIName string `json:"objectApiName"`
	UserID        string `json:"userId,omitempty"`

	OwnerUserIDs            []string `json:"ownerUserIds,omitempty"`
	GroupOwnerIDs           []string `json:"groupOwnerIds,omitempty"`
	SharingRuleOwnerIDs     []string `json:"sharingRuleOwnerIds,omitempty"`
	SharingRuleFromGroupIDs []string `json:"sharingRuleFromGroupIds,omitempty"`

	IncludeManualShares bool `json:"includeManualShares"`
	IncludeTeamMembers  bool `json:"includeTeamMembers"`

	AllowedIDs       []string `json:"allowedIds,omitempty"`
	AllowedIDsColumn string   `json:"allowedIdsColumn,omitempty"`

	// Presence-only for non-flat pointer fields (pointers themselves not serialized).
	HasParentFilter        bool `json:"hasParentFilter"`
	HasOwnerVisibility     bool `json:"hasOwnerVisibility"`
	HasTargetRecordFilters bool `json:"hasTargetRecordFilters"`

	// Rendered SQL from RLSResultToFilterExpr (empty qualifier).
	SQL string `json:"sql"`
}

type rlsRecordTwinCell struct {
	SharingModel string `json:"sharingModel"`
	Principal    string `json:"principal"`
	Object       string `json:"object"`
	RecordID     string `json:"recordId"`
	OwnerID      string `json:"ownerId"`

	CheckRead   bool `json:"checkRead"`
	CheckEdit   bool `json:"checkEdit"`
	CheckDelete bool `json:"checkDelete"`

	AllRead   bool `json:"allRead"`
	AllEdit   bool `json:"allEdit"`
	AllDelete bool `json:"allDelete"`
	AllLocked bool `json:"allLocked"`
}

// rlsMatrixSchema builds a plain (no-AccessPolicy) schema for a given sharing
// model. A plain schema returns nil RowReadPolicy and false UsesDerivedAccessPolicy,
// so the ownerVisibility / targetRecordRead / derived-AP branches are never taken —
// the cell exercises exactly the SharingModel arm under test. It always carries an
// OwnerId field (rls_filter.go:202) unless withoutOwner is set.
func rlsMatrixSchema(name string, model constants.SharingModel, withoutOwner bool) *models.ObjectMetadata {
	fields := []models.FieldMetadata{
		{APIName: constants.FieldID, IsSystem: true},
	}
	if !withoutOwner {
		fields = append(fields, models.FieldMetadata{APIName: constants.FieldOwnerID})
	}
	fields = append(fields, models.FieldMetadata{APIName: "Name", Createable: true, Updateable: true})
	return &models.ObjectMetadata{
		APIName:      name,
		SharingModel: model,
		Fields:       fields,
	}
}

// rlsMatrixCellFrom snapshots a live RLSResult into the deterministic golden DTO.
func rlsMatrixCellFrom(sharingModel, principal, object string, rls *RLSResult) rlsMatrixCell {
	sql := RLSResultToFilterExpr(rls, "")
	return rlsMatrixCell{
		SharingModel:            sharingModel,
		Principal:               principal,
		Object:                  object,
		Skip:                    rls.Skip,
		DenyAll:                 rls.DenyAll,
		HasOwnerField:           rls.HasOwnerField,
		ObjectAPIName:           rls.ObjectAPIName,
		UserID:                  rls.UserID,
		OwnerUserIDs:            sortedCopy(rls.OwnerUserIDs),
		GroupOwnerIDs:           sortedCopy(rls.GroupOwnerIDs),
		SharingRuleOwnerIDs:     sortedCopy(rls.SharingRuleOwnerIDs),
		SharingRuleFromGroupIDs: sortedCopy(rls.SharingRuleFromGroupIDs),
		IncludeManualShares:     rls.IncludeManualShares,
		IncludeTeamMembers:      rls.IncludeTeamMembers,
		AllowedIDs:              sortedCopy(rls.AllowedIDs),
		AllowedIDsColumn:        rls.AllowedIDsColumn,
		HasParentFilter:         rls.ParentFilter != nil,
		HasOwnerVisibility:      rls.OwnerVisibility != nil,
		HasTargetRecordFilters:  len(rls.TargetRecordFilters) > 0,
		SQL:                     sql,
	}
}

// rlsMatrixPrincipals returns the four principal arms as UserSessions plus the
// systemPerms/objectPerms wiring each needs, keyed off shared owner/admin IDs.
// SuperUserCapability == PermModifyAllData == "ModifyAllData" (permissions.go),
// so the superuser arm's systemPerms grant is exactly superUserCapabilityGrant().
type rlsMatrixPrincipal struct {
	name string
	user *models.UserSession
}

// CBP + folder fixture identifiers shared by the matrix golden. Kept as package
// constants so the schema wiring, the parent-record fixture, and the mock rows all
// agree on the same names/IDs.
const (
	cbpParentObject   = "ParentObjCBP" // parent object for the ControlledByParent child
	cbpParentFKField  = "ParentRef"    // OPTIONAL sharing-parent FK on the CBP child
	cbpParentRecordID = "parentcbp-1"  // parent record the CBP twin resolves through
	cbpFolderIDA      = "folder-a"     // known VisibleFolderIDs result (deterministic)
	cbpFolderIDB      = "folder-b"     // known VisibleFolderIDs result (deterministic)
)

// cbpMatrixMetadata declares a REAL sharing-parent relationship for the matrix's
// ControlledByParent child so BuildRLSResult returns a real ParentFilter (and the
// record twin resolves the parent record) instead of taking the malformed-edge
// deny-all path. All other metadata behavior is inherited from
// accessDecisionGoldenMetadata (which returns nil relationship / "" parent-controlled).
type cbpMatrixMetadata struct {
	accessDecisionGoldenMetadata
	child  string
	parent string
	field  string
}

func (m *cbpMatrixMetadata) GetSharingParentRelationship(_ context.Context, childAPI string) *models.Relationship {
	if childAPI == m.child {
		return &models.Relationship{
			ChildObjectAPIName:  m.child,
			ParentObjectAPIName: m.parent,
			FieldAPIName:        m.field,
			IsSharingParent:     true,
		}
	}
	return nil
}

func (m *cbpMatrixMetadata) GetParentControlledObject(_ context.Context, schema *models.ObjectMetadata) string {
	if schema != nil && schema.APIName == m.child &&
		schema.SharingModel == constants.SharingModelControlledByParent {
		return m.parent
	}
	return ""
}

// --- Section 3: AGREEMENT (security cross-check) ------------------------------

// TestRLSPolicyMatrixAgreement asserts that for each (principal, object, record),
// the single-record CheckRecordAccess(Read) decision AGREES with whether the
// query-level RLS (RLSResultToFilterExpr / ToSQLFilter) would admit that record.
// Where the admit decision needs a live DB (subquery arms), the cell is SKIPPED
// with a documented reason. This is the real security invariant: the row path and
// the query path must not disagree on whether a user can see a record.
func TestRLSPolicyMatrixAgreement(t *testing.T) {
	ctx := context.Background()

	ownerUser := &models.UserSession{ID: "user-owner", ProfileID: "profile-standard"}
	nonOwnerUser := &models.UserSession{ID: "user-nonowner", ProfileID: "profile-standard"}
	adminVAD := &models.UserSession{ID: "user-admin-vad", ProfileID: "profile-admin"}
	adminVAR := &models.UserSession{ID: "user-admin-var", ProfileID: "profile-admin"}
	superUser := &models.UserSession{ID: "user-superuser", ProfileID: "profile-superuser"}

	principals := []rlsMatrixPrincipal{
		{"owner", ownerUser},
		{"nonOwner", nonOwnerUser},
		{"adminViewAllData", adminVAD},
		{"adminViewAllRecords", adminVAR},
		{"superUser", superUser},
	}

	// Private + PublicReadWrite + PublicRead are the models whose read-admit can be
	// decided in-memory (Skip => admit all; DenyAll/"1 = 0" => admit none; owner-only
	// => record.OwnerId == user.ID). ControlledByParent/Folder* need a live DB or
	// folderAccess service to decide the admit, so they are handled by SKIP below.
	models3 := []struct {
		label string
		model constants.SharingModel
	}{
		{"PublicReadWrite", constants.SharingModelPublicReadWrite},
		{"PublicRead", constants.SharingModelPublicRead},
		{"Private", constants.SharingModelPrivate},
	}

	schemas := make([]*models.ObjectMetadata, 0, len(models3))
	objectFor := map[string]*models.ObjectMetadata{}
	for _, m := range models3 {
		objName := "Obj" + m.label
		sch := rlsMatrixSchema(objName, m.model, false)
		schemas = append(schemas, sch)
		objectFor[m.label] = sch
	}
	metadata := &accessDecisionGoldenMetadata{schemas: schemas}

	objectPerms := map[string]*models.ObjectPermissions{}
	for _, m := range models3 {
		objName := "Obj" + m.label
		objectPerms[accessDecisionGoldenKey(adminVAR.ID, objName)] = &models.ObjectPermissions{
			SobjectType:               objName,
			PermissionsRead:           true,
			PermissionsViewAllRecords: true,
		}
	}
	repo := &accessDecisionGoldenRepo{
		objectPerms: objectPerms,
		systemPerms: map[string]map[string]bool{
			adminVAD.ID:  {string(constants.PermViewAllData): true},
			superUser.ID: superUserCapabilityGrant(),
		},
	}
	svc := NewPermissionService(repo, metadata, nil)

	for _, m := range models3 {
		objName := "Obj" + m.label
		sch := objectFor[m.label]
		record := models.SObject{
			constants.FieldID:      "rec-" + m.label,
			constants.FieldOwnerID: ownerUser.ID,
		}
		for _, p := range principals {
			rowRead := svc.CheckRecordAccess(ctx, sch, record, constants.PermRead, p.user)
			rls := svc.BuildRLSResult(ctx, sch, objName, p.user)
			sql := RLSResultToFilterExpr(rls, "")

			admit, decidable, reason := admitInMemory(rls, sql, record, p.user)
			if !decidable {
				t.Logf("SKIP agreement (needs live DB): model=%s principal=%s reason=%s",
					m.label, p.name, reason)
				continue
			}
			if rowRead != admit {
				t.Fatalf("AGREEMENT VIOLATION model=%s principal=%s: CheckRecordAccess(Read)=%v but RLS admit=%v (sql=%q)",
					m.label, p.name, rowRead, admit, sql)
			}
		}
	}
}

// admitInMemory decides, without a DB, whether the RLS result/SQL would admit the
// given record. Returns (admit, decidable, reason). Not decidable when the SQL
// relies on a subquery arm (manual-share / team-member / group closure) or a
// non-flat filter that only a live DB can evaluate.
func admitInMemory(rls *RLSResult, sql string, record models.SObject, user *models.UserSession) (admit bool, decidable bool, reason string) {
	if rls.Skip || sql == "" {
		return true, true, "skip=admit-all"
	}
	if rls.DenyAll || sql == "1 = 0" {
		return false, true, "denyAll=admit-none"
	}
	// Any subquery/semi-join arm requires a live DB to evaluate.
	if rls.IncludeManualShares || rls.IncludeTeamMembers ||
		len(rls.SharingRuleFromGroupIDs) > 0 || rls.ParentFilter != nil ||
		len(rls.TargetRecordFilters) > 0 || rls.OwnerVisibility != nil ||
		len(rls.CriteriaSQLFragments) > 0 || len(rls.AllowedIDs) > 0 {
		return false, false, "has-subquery-or-nonflat-arm"
	}
	// Owner-only fast path: the only admitting predicate is OwnerId IN {...}.
	ownerID, _ := record[constants.FieldOwnerID].(string)
	if rls.HasOwnerField {
		for _, id := range rls.OwnerUserIDs {
			if id == ownerID {
				return true, true, "owner-match"
			}
		}
		for _, id := range rls.SharingRuleOwnerIDs {
			if id == ownerID {
				return true, true, "sharing-rule-owner-match"
			}
		}
		for _, id := range rls.GroupOwnerIDs {
			if id == ownerID {
				return true, true, "group-owner-match"
			}
		}
		return false, true, "no-owner-match"
	}
	return false, true, "no-owner-field-and-no-skip"
}

// --- Section 4: FAIL-CLOSED / never-over-Skip --------------------------------

// TestRLSPolicyMatrixFailClosed pins the fail-closed invariants: the listed
// degenerate inputs MUST resolve to DenyAll (never Skip=true), and Skip=true may
// appear ONLY for the whitelisted privileged arms. This is the over-grant guard —
// if a future refactor accidentally returns Skip for a non-privileged cell, this
// test fails.
func TestRLSPolicyMatrixFailClosed(t *testing.T) {
	ctx := context.Background()

	user := &models.UserSession{ID: "user-1", ProfileID: "profile-standard"}
	privateWithOwner := rlsMatrixSchema("PrivObj", constants.SharingModelPrivate, false)
	privateNoOwner := rlsMatrixSchema("PrivNoOwner", constants.SharingModelPrivate, true)
	folderShare := rlsMatrixSchema("FolderObj", constants.SharingModelFolderSharing, false)

	metadata := &accessDecisionGoldenMetadata{schemas: []*models.ObjectMetadata{
		privateWithOwner, privateNoOwner, folderShare,
	}}
	repo := &accessDecisionGoldenRepo{}
	svc := NewPermissionService(repo, metadata, nil)

	// (a) nil schema => DenyAll.
	if r := svc.BuildRLSResult(ctx, nil, "Whatever", user); !r.DenyAll || r.Skip {
		t.Fatalf("nil schema: want DenyAll (not Skip), got %+v", r)
	}
	// (b) nil user => DenyAll.
	if r := svc.BuildRLSResult(ctx, privateWithOwner, "PrivObj", nil); !r.DenyAll || r.Skip {
		t.Fatalf("nil user: want DenyAll (not Skip), got %+v", r)
	}
	// (c) recursion cycle: revisiting the same objectName on a pre-seeded visited map.
	//     Drive through the exported entry using a self-referential ControlledByParent
	//     schema so the cycle guard (rls_filter.go:73-80) fires on the second visit.
	cycleSchema := rlsMatrixSchema("CycleObj", constants.SharingModelControlledByParent, false)
	cycleMeta := &accessDecisionGoldenMetadata{schemas: []*models.ObjectMetadata{cycleSchema}}
	// A sharing-parent relationship pointing back to itself makes the parent build
	// re-enter the same objectName; the visited guard must resolve to DenyAll.
	cycleSvc := NewPermissionService(&accessDecisionGoldenRepo{}, &selfParentMetadata{
		accessDecisionGoldenMetadata: *cycleMeta,
		child:                        "CycleObj",
		parent:                       "CycleObj",
		field:                        "ParentId",
	}, nil)
	if r := cycleSvc.BuildRLSResult(ctx, cycleSchema, "CycleObj", user); !r.DenyAll {
		// The self-referential parent edge re-enters CycleObj; the inner visit is
		// guarded and produces DenyAll for the parent, which the ParentFilter carries.
		// The OUTER result is not itself DenyAll (it carries a ParentFilter), so assert
		// the INNER (parent) result is DenyAll instead — see below.
		if r.ParentFilter == nil || r.ParentFilter.ParentRLSResult == nil || !r.ParentFilter.ParentRLSResult.DenyAll {
			t.Fatalf("recursion cycle: want inner parent DenyAll, got outer=%+v", r)
		}
	}
	// (d) FolderSharing with folderAccess nil => DenyAll (fail-closed).
	if r := svc.BuildRLSResult(ctx, folderShare, "FolderObj", user); !r.DenyAll || r.Skip {
		t.Fatalf("FolderSharing (folderAccess nil): want DenyAll (not Skip), got %+v", r)
	}
	// (e) Private schema with NO OwnerId field => SQL renders "1 = 0" (admit none),
	//     even though the RLSResult itself is not DenyAll (it has no owner arm).
	noOwnerRes := svc.BuildRLSResult(ctx, privateNoOwner, "PrivNoOwner", user)
	if noOwnerRes.Skip {
		t.Fatalf("Private no-owner: must not Skip, got %+v", noOwnerRes)
	}
	// With no owner field and share/team arms dropped (empty probes), SQL is "1 = 0".
	if sql := RLSResultToFilterExpr(noOwnerRes, ""); sql != "1 = 0" {
		t.Fatalf("Private no-owner: want SQL \"1 = 0\", got %q (result=%+v)", sql, noOwnerRes)
	}

	// (f) Over-grant guard: Skip==true ONLY for whitelisted privileged arms.
	//     Sweep the full taxonomy grid and assert every non-privileged cell has
	//     Skip==false.
	assertSkipOnlyForPrivilegedArms(ctx, t)
}

// privilegedSkipArm names the (principal) arms that are ALLOWED to produce Skip=true:
//   - SuperUser                (rls_filter.go:94)
//   - ownerVisibility-admin    (rls_filter.go:138)  [not exercised by plain schemas]
//   - Public / PublicRead      (rls_filter.go:158)
//   - ViewAllData              (rls_filter.go:181)
//   - ViewAllRecords           (rls_filter.go:192)
func assertSkipOnlyForPrivilegedArms(ctx context.Context, t *testing.T) {
	t.Helper()

	ownerUser := &models.UserSession{ID: "user-owner", ProfileID: "profile-standard"}
	nonOwnerUser := &models.UserSession{ID: "user-nonowner", ProfileID: "profile-standard"}
	adminVAD := &models.UserSession{ID: "user-admin-vad", ProfileID: "profile-admin"}
	adminVAR := &models.UserSession{ID: "user-admin-var", ProfileID: "profile-admin"}
	superUser := &models.UserSession{ID: "user-superuser", ProfileID: "profile-superuser"}

	type arm struct {
		principal  string
		user       *models.UserSession
		privileged bool // may legitimately Skip via a user-level privilege arm
	}
	arms := []arm{
		{"owner", ownerUser, false},
		{"nonOwner", nonOwnerUser, false},
		{"adminViewAllData", adminVAD, true},
		{"adminViewAllRecords", adminVAR, true},
		{"superUser", superUser, true},
	}

	models6 := []struct {
		label       string
		model       constants.SharingModel
		modelPublic bool // Public/PublicRead legitimately Skip for ALL principals
	}{
		{"PublicReadWrite", constants.SharingModelPublicReadWrite, true},
		{"PublicRead", constants.SharingModelPublicRead, true},
		{"Private", constants.SharingModelPrivate, false},
		{"ControlledByParent", constants.SharingModelControlledByParent, false},
		{"FolderSharing", constants.SharingModelFolderSharing, false},
		{"FolderContentSharing", constants.SharingModelFolderContentSharing, false},
	}

	schemas := make([]*models.ObjectMetadata, 0, len(models6))
	objectFor := map[string]*models.ObjectMetadata{}
	for _, m := range models6 {
		objName := "Obj" + m.label
		sch := rlsMatrixSchema(objName, m.model, false)
		schemas = append(schemas, sch)
		objectFor[m.label] = sch
	}
	metadata := &accessDecisionGoldenMetadata{schemas: schemas}

	objectPerms := map[string]*models.ObjectPermissions{}
	for _, m := range models6 {
		objName := "Obj" + m.label
		objectPerms[accessDecisionGoldenKey(adminVAR.ID, objName)] = &models.ObjectPermissions{
			SobjectType:               objName,
			PermissionsRead:           true,
			PermissionsViewAllRecords: true,
		}
	}
	repo := &accessDecisionGoldenRepo{
		objectPerms: objectPerms,
		systemPerms: map[string]map[string]bool{
			adminVAD.ID:  {string(constants.PermViewAllData): true},
			superUser.ID: superUserCapabilityGrant(),
		},
	}
	svc := NewPermissionService(repo, metadata, nil)

	for _, m := range models6 {
		objName := "Obj" + m.label
		sch := objectFor[m.label]
		for _, a := range arms {
			rls := svc.BuildRLSResult(ctx, sch, objName, a.user)
			if !rls.Skip {
				continue
			}
			// Skip=true is only legitimate when the MODEL is Public(Read(Write)) OR
			// the PRINCIPAL holds a user-level privilege (VAD/VAR/SuperUser).
			if m.modelPublic || a.privileged {
				continue
			}
			t.Fatalf("OVER-GRANT GUARD: Skip=true for non-privileged cell model=%s principal=%s (result=%+v)",
				m.label, a.principal, rls)
		}
	}
}

// selfParentMetadata makes a ControlledByParent schema point its sharing parent
// back at itself, so BuildRLSResult re-enters the same objectName and the cycle
// guard must fire.
type selfParentMetadata struct {
	accessDecisionGoldenMetadata
	child  string
	parent string
	field  string
}

func (m *selfParentMetadata) GetSharingParentRelationship(_ context.Context, childAPI string) *models.Relationship {
	if childAPI == m.child {
		return &models.Relationship{
			ChildObjectAPIName:  m.child,
			ParentObjectAPIName: m.parent,
			FieldAPIName:        m.field,
			IsSharingParent:     true,
		}
	}
	return nil
}

func (m *selfParentMetadata) GetParentControlledObject(_ context.Context, schema *models.ObjectMetadata) string {
	if schema != nil && schema.APIName == m.child {
		return m.parent
	}
	return ""
}

// --- Section 5: #2585 arm-drop preservation ----------------------------------

// TestRLSPolicyMatrixArmDropDirections pins BOTH directions of the issue #2585
// empty-arm-drop: a probe returning (empty, nil) flips the arm to FALSE, while a
// probe returning (nil, error) keeps the arm TRUE (fail-safe). The empty->false
// direction reuses pcExistenceRepo; the error->true direction uses a small
// error-returning repo wrapper.
func TestRLSPolicyMatrixArmDropDirections(t *testing.T) {
	ctx := context.Background()
	schema := &models.ObjectMetadata{
		APIName:      "Widget",
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, IsSystem: true},
			{APIName: constants.FieldOwnerID},
		},
	}
	md := &pcMetadataStub{schemas: map[string]*models.ObjectMetadata{"Widget": schema}}
	user := &models.UserSession{ID: "005-user", ProfileID: "00e-standard"}

	// Direction 1: (empty, nil) => arms flip to FALSE (reuse pcExistenceRepo).
	drop := makePCService(&pcExistenceRepo{}, md).BuildRLSResult(ctx, schema, "Widget", user)
	if drop.IncludeManualShares || drop.IncludeTeamMembers {
		t.Fatalf("(empty,nil) probes must DROP arms, got IncludeManualShares=%v IncludeTeamMembers=%v",
			drop.IncludeManualShares, drop.IncludeTeamMembers)
	}

	// Direction 2: (nil, error) => arms STAY true (fail-safe).
	keep := makePCService(&armProbeErrorRepo{}, md).BuildRLSResult(ctx, schema, "Widget", user)
	if !keep.IncludeManualShares || !keep.IncludeTeamMembers {
		t.Fatalf("(nil,error) probes must KEEP arms (fail-safe), got IncludeManualShares=%v IncludeTeamMembers=%v",
			keep.IncludeManualShares, keep.IncludeTeamMembers)
	}
}

// armProbeErrorRepo makes the manual-share / team-member existence probes return
// (nil, error) so the #2585 fail-safe branch keeps the arms.
type armProbeErrorRepo struct {
	pcRepo
}

func (r *armProbeErrorRepo) GetManualShareRecordIDs(context.Context, string, string) ([]string, error) {
	return nil, errors.New("boom")
}

func (r *armProbeErrorRepo) GetTeamMemberRecordIDs(context.Context, string, string) ([]string, error) {
	return nil, errors.New("boom")
}
