package records

import (
	"context"
	"errors"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/recordsguard"
)

// guardEntriesFn is a small helper that produces a loader returning the given
// entries unconditionally. Mirrors the lightweight per-test fake style used
// by other records-package tests (see insert_inheritance_preview_test.go).
func guardEntriesFn(entries ...recordsguard.CreateFromSourceGuardEntry) createFromSourceGuardLoader {
	return func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		return entries, nil
	}
}

// owningEntry returns a single configured guard entry for a clean test fixture.
func owningEntry(sourceFK, actionID, name string) recordsguard.CreateFromSourceGuardEntry {
	return recordsguard.CreateFromSourceGuardEntry{
		SourceFKField:          sourceFK,
		OwningStandardActionId: actionID,
		ActionName:             name,
	}
}

type runtimeGuardProvider struct {
	loads   int
	entries []recordsguard.CreateFromSourceGuardEntry
}

func (p *runtimeGuardProvider) CreateFromSourceGuardEntries(context.Context, string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
	p.loads++
	return append([]recordsguard.CreateFromSourceGuardEntry(nil), p.entries...), nil
}

func TestCreateFromSourceGuardRuntimeProviderIsSoleRevisionBoundReadPath(t *testing.T) {
	provider := &runtimeGuardProvider{entries: []recordsguard.CreateFromSourceGuardEntry{
		owningEntry("ParentRef__c", "sa_runtime", "createTargetFromSource"),
	}}
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(func(context.Context, string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		t.Fatal("legacy/live guard loader must not run when Runtime metadata is installed")
		return nil, nil
	})
	if err := ps.SetCreateFromSourceGuardRuntime(provider); err != nil {
		t.Fatal(err)
	}

	for i := 0; i < 2; i++ {
		err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{"ParentRef__c": "source-1"})
		assertInvalidOperation(t, err)
	}
	if provider.loads != 4 {
		// Each request resolves candidate fields and then complete entries from
		// its bound immutable view; no process-global result survives revisions.
		t.Fatalf("runtime provider loads = %d, want 4 request-bound reads", provider.loads)
	}
}

// assertInvalidOperation verifies the guard produced a neutral
// INVALID_OPERATION rejection. The v63 INVALID_OPERATION code, HTTP status, and
// concrete action endpoint URL are owned by the REST boundary, not this engine.
func assertInvalidOperation(t *testing.T, err error) string {
	t.Helper()
	if err == nil {
		t.Fatal("guard returned nil error; want INVALID_OPERATION")
	}
	var domainErr *apperrors.DomainWireError
	if !errors.As(err, &domainErr) {
		t.Fatalf("guard returned %T; want *apperrors.DomainWireError. err=%v", err, err)
	}
	if domainErr.Kind != apperrors.WireInvalidOperation {
		t.Fatalf("Kind = %q; want %q", domainErr.Kind, apperrors.WireInvalidOperation)
	}
	if domainErr.CreateFromSourceGuard == nil {
		t.Fatalf("CreateFromSourceGuard details missing from %T", err)
	}
	if strings.Contains(domainErr.Message, "/services/data/") {
		t.Fatalf("neutral guard message leaked a concrete REST endpoint: %q", domainErr.Message)
	}
	return domainErr.Message
}

// F1: Path A, no ctx action, body sets the configured source-FK. Must reject
// with INVALID_OPERATION naming the configured action.
func TestGuardCreateFromSourceContext_RejectsSourceFKWithoutOwningActionContext(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	))

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	msg := assertInvalidOperation(t, err)
	if !strings.Contains(msg, "createTargetFromSource") {
		t.Fatalf("message %q does not name the owning action", msg)
	}
}

// F2: Path A, no entries for T. Plain create — guard returns nil.
func TestGuardCreateFromSourceContext_AllowsPlainCreateWhenNoConfiguredAction(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn())

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (no configured entries)", err)
	}
}

// F3: Path A, body omits the configured source-FK. Plain create — guard
// returns nil even though entries exist for T.
func TestGuardCreateFromSourceContext_AllowsPlainCreateWhenSourceFKBlank(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	))

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"Name": "x",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (source-FK absent)", err)
	}

	// Also: empty string FK is treated as blank (no bypass attempt).
	err = ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"Name":         "x",
		"ParentRef__c": "   ",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (source-FK whitespace-only)", err)
	}
}

func TestGuardCreateFromSourceContext_SkipsFullLoadWhenCandidateFKAbsent(t *testing.T) {
	ps := &PersistenceService{}
	candidateLoads := 0
	entryLoads := 0
	ps.SetGuardCandidateFieldLoader(func(_ context.Context, _ string) ([]string, error) {
		candidateLoads++
		return []string{"ParentRef__c"}, nil
	})
	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		entryLoads++
		return nil, errors.New("full guard loader should stay lazy")
	})

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"Name": "plain",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (candidate FK absent)", err)
	}
	if candidateLoads != 1 {
		t.Fatalf("candidate loader calls = %d; want 1", candidateLoads)
	}
	if entryLoads != 0 {
		t.Fatalf("entry loader calls = %d; want 0 when body omits candidate FK", entryLoads)
	}
}

func TestGuardCreateFromSourceContext_CachesLoaderByTargetObject(t *testing.T) {
	ps := &PersistenceService{}
	loadsByObject := make(map[string]int)
	ps.SetGuardEntryLoader(func(_ context.Context, objectName string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		loadsByObject[objectName]++
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
		}, nil
	})

	for i := 0; i < 2; i++ {
		err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{"Name": "plain"})
		if err != nil {
			t.Fatalf("guard returned %v; want nil (source-FK absent)", err)
		}
	}
	if loadsByObject["T"] != 1 {
		t.Fatalf("loader calls for T = %d; want 1 cached load", loadsByObject["T"])
	}

	err := ps.guardCreateFromSourceContext(context.Background(), "Other", models.SObject{"Name": "plain"})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (source-FK absent)", err)
	}
	if loadsByObject["Other"] != 1 {
		t.Fatalf("loader calls for Other = %d; want 1 object-specific load", loadsByObject["Other"])
	}
}

func TestCreateFromSourceGuardCacheInvalidatesOnMetadataObjectMutation(t *testing.T) {
	ps := &PersistenceService{}
	loads := 0
	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		loads++
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
		}, nil
	})

	for i := 0; i < 2; i++ {
		err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{"Name": "plain"})
		if err != nil {
			t.Fatalf("guard returned %v; want nil (source-FK absent)", err)
		}
	}
	if loads != 1 {
		t.Fatalf("loader calls before invalidation = %d; want 1 cached load", loads)
	}

	ps.invalidateSecurityCachesForChanges(context.Background(), constants.TableObjectFieldMapping, nil)
	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{"Name": "plain"})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (source-FK absent)", err)
	}
	if loads != 2 {
		t.Fatalf("loader calls after ObjectFieldMapping invalidation = %d; want 2", loads)
	}

	ps.invalidateSecurityCachesForChanges(context.Background(), constants.TableUser, nil)
	err = ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{"Name": "plain"})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (source-FK absent)", err)
	}
	if loads != 2 {
		t.Fatalf("loader calls after unrelated metadata invalidation = %d; want cache retained at 2", loads)
	}
}

func TestCreateFromSourceGuardCacheDiscardsLoadRacingWithInvalidation(t *testing.T) {
	ps := &PersistenceService{}
	loads := 0
	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		loads++
		if loads == 1 {
			ps.InvalidateCreateFromSourceGuardCache()
			return []recordsguard.CreateFromSourceGuardEntry{
				owningEntry("ParentRef__c", "sa_old", "oldAction"),
			}, nil
		}
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("ParentRef__c", "sa_new", "newAction"),
		}, nil
	})

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	msg := assertInvalidOperation(t, err)
	if strings.Contains(msg, "oldAction") || !strings.Contains(msg, "newAction") {
		t.Fatalf("message %q must use post-invalidation guard entries", msg)
	}
	if loads != 2 {
		t.Fatalf("loader calls after racing invalidation = %d; want stale load discarded and reloaded", loads)
	}

	err = ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	msg = assertInvalidOperation(t, err)
	if !strings.Contains(msg, "newAction") {
		t.Fatalf("message %q must use cached post-invalidation guard entries", msg)
	}
	if loads != 2 {
		t.Fatalf("loader calls after cached post-invalidation read = %d; want 2", loads)
	}
}

func TestCreateFromSourceGuardEntryCachePostInvalidateReadDoesNotJoinPriorEpochLoad(t *testing.T) {
	ps := &PersistenceService{}

	var loads int32
	firstStarted := make(chan struct{})
	secondStarted := make(chan struct{})
	releaseFirst := make(chan struct{})
	released := false
	release := func() {
		if !released {
			close(releaseFirst)
			released = true
		}
	}
	defer release()

	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		switch atomic.AddInt32(&loads, 1) {
		case 1:
			close(firstStarted)
			<-releaseFirst
			return []recordsguard.CreateFromSourceGuardEntry{
				owningEntry("ParentRef__c", "sa_old", "oldAction"),
			}, nil
		case 2:
			close(secondStarted)
			return []recordsguard.CreateFromSourceGuardEntry{
				owningEntry("ParentRef__c", "sa_new", "newAction"),
			}, nil
		default:
			t.Fatalf("entry guard loads = %d; want at most 2", loads)
			return nil, nil
		}
	})

	resultCh := make(chan error, 2)
	runGuard := func() {
		resultCh <- ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
			"ParentRef__c": "src-1",
		})
	}

	go runGuard()
	select {
	case <-firstStarted:
	case <-time.After(time.Second):
		t.Fatal("first create-from-source entry load did not start")
	}

	ps.InvalidateCreateFromSourceGuardCache()
	go runGuard()

	select {
	case <-secondStarted:
	case <-time.After(time.Second):
		t.Fatal("post-invalidation entry read reused the prior-epoch in-flight load")
	}

	release()

	for i := 0; i < 2; i++ {
		select {
		case err := <-resultCh:
			msg := assertInvalidOperation(t, err)
			if strings.Contains(msg, "oldAction") || !strings.Contains(msg, "newAction") {
				t.Fatalf("message %q must use post-invalidation guard entries", msg)
			}
		case <-time.After(time.Second):
			t.Fatal("timed out waiting for create-from-source entry result")
		}
	}

	if got := atomic.LoadInt32(&loads); got != 2 {
		t.Fatalf("entry guard loads = %d; want 2 distinct epoch loads", got)
	}
}

func TestCreateFromSourceGuardCandidateCachePostInvalidateReadDoesNotJoinPriorEpochLoad(t *testing.T) {
	ps := &PersistenceService{}

	var candidateLoads int32
	firstStarted := make(chan struct{})
	secondStarted := make(chan struct{})
	releaseFirst := make(chan struct{})
	released := false
	release := func() {
		if !released {
			close(releaseFirst)
			released = true
		}
	}
	defer release()

	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_new", "newAction"),
	))
	ps.SetGuardCandidateFieldLoader(func(_ context.Context, _ string) ([]string, error) {
		switch atomic.AddInt32(&candidateLoads, 1) {
		case 1:
			close(firstStarted)
			<-releaseFirst
			return []string{"LegacyRef__c"}, nil
		case 2:
			close(secondStarted)
			return []string{"ParentRef__c"}, nil
		default:
			t.Fatalf("candidate guard loads = %d; want at most 2", candidateLoads)
			return nil, nil
		}
	})

	resultCh := make(chan error, 2)
	runGuard := func() {
		resultCh <- ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
			"ParentRef__c": "src-1",
		})
	}

	go runGuard()
	select {
	case <-firstStarted:
	case <-time.After(time.Second):
		t.Fatal("first create-from-source candidate load did not start")
	}

	ps.InvalidateCreateFromSourceGuardCache()
	go runGuard()

	select {
	case <-secondStarted:
	case <-time.After(time.Second):
		t.Fatal("post-invalidation candidate read reused the prior-epoch in-flight load")
	}

	release()

	for i := 0; i < 2; i++ {
		select {
		case err := <-resultCh:
			msg := assertInvalidOperation(t, err)
			if !strings.Contains(msg, "newAction") {
				t.Fatalf("message %q must use post-invalidation guard action", msg)
			}
		case <-time.After(time.Second):
			t.Fatal("timed out waiting for create-from-source candidate result")
		}
	}

	if got := atomic.LoadInt32(&candidateLoads); got != 2 {
		t.Fatalf("candidate guard loads = %d; want 2 distinct epoch loads", got)
	}
}

// F4: Path A, ctx carries the owning action's id. Bypass branch — guard
// returns nil.
func TestGuardCreateFromSourceContext_BypassesWhenCtxCarriesOwningActionID(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	))

	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "sa_act1", "Std_Act1")
	err := ps.guardCreateFromSourceContext(ctx, "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (ctx carries owning action id)", err)
	}
}

func TestGuardCreateFromSourceContext_BypassesCatalogActionWithMatchingRef(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "legacy-database-action-id", "createTargetFromSource"),
	))

	ctx := constants.WithStandardActionExecutionRootFlow(
		context.Background(), "catalog-action-sha256-v1:opaque", "Std_Act1",
	)
	ctx = constants.WithStandardActionExecutionRef(ctx, "createTargetFromSource")
	err := ps.guardCreateFromSourceContext(ctx, "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil for authorized Catalog action ref", err)
	}
}

// F5: Path A, ctx carries a different action's id. Mismatch — guard rejects
// with INVALID_OPERATION naming the configured (owning) action, NOT the
// unrelated ctx action.
func TestGuardCreateFromSourceContext_RejectsWhenCtxCarriesUnrelatedActionID(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	))

	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "sa_other", "Std_Other")
	err := ps.guardCreateFromSourceContext(ctx, "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	msg := assertInvalidOperation(t, err)
	if !strings.Contains(msg, "createTargetFromSource") {
		t.Fatalf("message %q must name the owning action, not the ctx action", msg)
	}
	if strings.Contains(msg, "sa_other") || strings.Contains(msg, "Std_Other") {
		t.Fatalf("message %q must not leak the unrelated ctx action identifier", msg)
	}
}

// F11(b): multi-owner — two distinct actions both author entries on the same
// (TargetObject, SourceFKField). Ctx matches one → bypass. Mirrors the design's
// F11(b) sub-test.
func TestGuardCreateFromSourceContext_AcceptsAnyOwningActionInMultiOwnerGroup(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_actA", "actionA"),
		owningEntry("ParentRef__c", "sa_actB", "actionB"),
	))

	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "sa_actB", "Std_ActB")
	err := ps.guardCreateFromSourceContext(ctx, "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (multi-owner, ctx matches actB)", err)
	}
}

// F11(c): multi-owner — ctx matches NONE of the owning actions. Reject AND
// the error message names BOTH owning actions (set-equality), not just one.
func TestGuardCreateFromSourceContext_RejectsWhenCtxMatchesNoOwnerInMultiOwnerGroup(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_actA", "actionA"),
		owningEntry("ParentRef__c", "sa_actB", "actionB"),
	))

	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "sa_unrelated", "Std_Unrelated")
	err := ps.guardCreateFromSourceContext(ctx, "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	msg := assertInvalidOperation(t, err)
	wantActions := []string{
		"actionA",
		"actionB",
	}
	for _, want := range wantActions {
		if !strings.Contains(msg, want) {
			t.Fatalf("message %q missing action %q (set-equality on multi-owner group)", msg, want)
		}
	}
}

// Decoupled-FK group: when entries exist on two different SourceFKFields and
// the body only sets one, only the matching group fires; the unrelated entry
// does not contribute to the rejection.
func TestGuardCreateFromSourceContext_RejectsOnlyMatchingSourceFKGroup(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRefA__c", "sa_actA", "actionA"),
		owningEntry("ParentRefB__c", "sa_actB", "actionB"),
	))

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRefA__c": "src-1",
	})
	msg := assertInvalidOperation(t, err)
	if !strings.Contains(msg, "actionA") {
		t.Fatalf("message %q must name actionA (matched FK)", msg)
	}
	if strings.Contains(msg, "actionB") {
		t.Fatalf("message %q must NOT name actionB (unrelated FK group)", msg)
	}
}

// Loader error path: if the loader fails, the guard surfaces the failure as
// an error (NOT a silent allow) so DB outages don't quietly bypass the guard.
func TestGuardCreateFromSourceContext_LoaderErrorPropagates(t *testing.T) {
	ps := &PersistenceService{}
	wantErr := errors.New("loader exploded")
	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		return nil, wantErr
	})

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	if err == nil {
		t.Fatal("guard returned nil; want loader error to surface")
	}
	if !errors.Is(err, wantErr) {
		t.Fatalf("guard returned %v; want wrapped %v", err, wantErr)
	}
}

// Nil rawDB / no test seam → default loader returns nil entries → no-op.
// Verifies the test-harness contract documented in
// defaultCreateFromSourceGuardLoader.
func TestGuardCreateFromSourceContext_NilRawDBIsNoOp(t *testing.T) {
	ps := &PersistenceService{} // rawDB nil, no loader set.

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (nil rawDB ⇒ no-op)", err)
	}
}

// Entry with a non-string FK value in the body — the guard treats numeric /
// nil / interface values as blank and does NOT fire. The body shape from the
// REST layer is always string-keyed for FK fields (normalized upstream); this
// test pins the defensive contract.
func TestGuardCreateFromSourceContext_TreatsNonStringFKAsBlank(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	))

	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": 12345, // numeric, not a string id
	})
	if err != nil {
		t.Fatalf("guard returned %v; want nil (non-string FK is treated as blank)", err)
	}
}

// F2 (bulk-path): verify the wrapper delegates to the variant after loading
// entries. This is the regression guard that the refactor (extract variant,
// thin wrapper) did not break the existing Path A contract.
func TestGuardCreateFromSourceContext_WrapperDelegatesToVariant(t *testing.T) {
	ps := &PersistenceService{}
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	))

	// Same inputs as F1; the wrapper must load then delegate.
	err := ps.guardCreateFromSourceContext(context.Background(), "T", models.SObject{
		"ParentRef__c": "src-1",
	})
	msg := assertInvalidOperation(t, err)
	if !strings.Contains(msg, "createTargetFromSource") {
		t.Fatalf("message %q does not name the owning action", msg)
	}
}

// F4 (bulk-path variant): preload error propagation. The variant surfaces a
// cached load error on every record, and the total loader call count is 1.
func TestGuardCreateFromSourceContextWithEntries_PreloadErrorPropagates(t *testing.T) {
	wantErr := errors.New("loader exploded")
	entries := []recordsguard.CreateFromSourceGuardEntry{
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	}

	// Verify: loadErr != nil → surfaces the same wrapped-error envelope.
	err := guardCreateFromSourceContextWithEntries(
		context.Background(), "T",
		models.SObject{"ParentRef__c": "src-1"},
		entries, wantErr,
	)
	if err == nil {
		t.Fatal("variant returned nil; want loader error to surface")
	}
	if !errors.Is(err, wantErr) {
		t.Fatalf("variant returned %v; want wrapped %v", err, wantErr)
	}

	// Verify: loadErr == nil, entries == nil → no-op (cached empty load).
	err = guardCreateFromSourceContextWithEntries(
		context.Background(), "T",
		models.SObject{"ParentRef__c": "src-1"},
		nil, nil,
	)
	if err != nil {
		t.Fatalf("variant returned %v; want nil (cached empty load)", err)
	}
}

// F3 (bulk-path variant): partial-failure envelope. Preloaded entries cause
// a reject on a record carrying the source-FK; a record without the source-FK
// passes. BulkInsert's per-record caller wraps this into FailedCount / Errors.
func TestGuardCreateFromSourceContextWithEntries_PartialFailureEnvelope(t *testing.T) {
	entries := []recordsguard.CreateFromSourceGuardEntry{
		owningEntry("ParentRef__c", "sa_act1", "createTargetFromSource"),
	}

	// Record WITH source-FK → reject.
	err := guardCreateFromSourceContextWithEntries(
		context.Background(), "T",
		models.SObject{"ParentRef__c": "src-1", "Name": "bad"},
		entries, nil,
	)
	msg := assertInvalidOperation(t, err)
	if !strings.Contains(msg, "createTargetFromSource") {
		t.Fatalf("message %q does not name the owning action", msg)
	}

	// Record WITHOUT source-FK → allow.
	err = guardCreateFromSourceContextWithEntries(
		context.Background(), "T",
		models.SObject{"Name": "good"},
		entries, nil,
	)
	if err != nil {
		t.Fatalf("variant returned %v; want nil (no source-FK set)", err)
	}
}

// F8 (bulk-path variant): multi-owner FK. Preloaded entries with two owners;
// ctx matching one → bypass; ctx matching none → reject naming both.
func TestGuardCreateFromSourceContextWithEntries_MultiOwnerOnBulkPath(t *testing.T) {
	entries := []recordsguard.CreateFromSourceGuardEntry{
		owningEntry("ParentRef__c", "sa_actA", "actionA"),
		owningEntry("ParentRef__c", "sa_actB", "actionB"),
	}

	// Ctx carries actB → bypass.
	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "sa_actB", "Std_ActB")
	err := guardCreateFromSourceContextWithEntries(
		ctx, "T",
		models.SObject{"ParentRef__c": "src-1"},
		entries, nil,
	)
	if err != nil {
		t.Fatalf("variant returned %v; want nil (multi-owner, ctx matches actB)", err)
	}

	// Ctx carries unrelated action → reject, both actions named.
	ctx = constants.WithStandardActionExecutionRootFlow(context.Background(), "sa_unrelated", "Std_Unrelated")
	err = guardCreateFromSourceContextWithEntries(
		ctx, "T",
		models.SObject{"ParentRef__c": "src-1"},
		entries, nil,
	)
	msg := assertInvalidOperation(t, err)
	for _, want := range []string{"actionA", "actionB"} {
		if !strings.Contains(msg, want) {
			t.Fatalf("message %q missing action %q (multi-owner)", msg, want)
		}
	}
}

func TestBulkInsertCreateFromSourceGuardRunsWhenSkipValidation(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      "TargetObject",
		Label:        "Target Object",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: "SourceId", Type: constants.FieldTypeReference, ReferenceTo: []string{"SourceObject"}, Createable: true, Updateable: true},
		},
	}
	repo := &captureRecordRepo{}
	ps := newRecordEntrypointTestService(schema, repo)
	loads := 0
	ps.SetGuardEntryLoader(func(_ context.Context, objectName string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		loads++
		if objectName != "TargetObject" {
			t.Fatalf("loader objectName = %q; want TargetObject", objectName)
		}
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("SourceId", "sa_createTarget", "createTarget"),
		}, nil
	})

	result, err := ps.BulkInsert(
		context.Background(),
		"TargetObject",
		[]models.SObject{{"SourceId": "src-1"}},
		recordEntrypointUser(),
		BulkInsertOptions{SkipValidation: true},
	)
	if err == nil {
		t.Fatal("BulkInsert err = nil; want all-records-failed validation error")
	}
	if result.SuccessCount != 0 || result.FailedCount != 1 {
		t.Fatalf("result counts = success %d failed %d; want success 0 failed 1", result.SuccessCount, result.FailedCount)
	}
	if loads != 1 {
		t.Fatalf("guard loader calls = %d; want 1", loads)
	}
	if len(result.Errors) != 1 ||
		!strings.Contains(result.Errors[0], "Direct create of TargetObject") ||
		!strings.Contains(result.Errors[0], "createTarget") {
		t.Fatalf("result.Errors = %#v; want direct-create rejection naming createTarget", result.Errors)
	}
	if len(repo.bulkInserted) != 0 {
		t.Fatalf("bulkInserted = %#v; want no DML after guard rejection", repo.bulkInserted)
	}
}

func TestBulkInsertCreateFromSourceGuardRejectsBeforeSaveSourceFK(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      "TargetObject",
		Label:        "Target Object",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{APIName: "SourceId", Type: constants.FieldTypeReference, ReferenceTo: []string{"SourceObject"}, Createable: true, Updateable: true},
		},
	}
	repo := &captureRecordRepo{}
	ps := newRecordEntrypointTestService(schema, repo)

	var beforeCreateCount int
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType != sharedEvents.RecordBeforeCreate {
			return
		}
		beforeCreateCount++
		record["SourceId"] = "src-flow"
	}}

	candidateLoads := 0
	entryLoads := 0
	ps.SetGuardCandidateFieldLoader(func(_ context.Context, objectName string) ([]string, error) {
		candidateLoads++
		if objectName != "TargetObject" {
			t.Fatalf("candidate loader objectName = %q; want TargetObject", objectName)
		}
		return []string{"SourceId"}, nil
	})
	ps.SetGuardEntryLoader(func(_ context.Context, objectName string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		entryLoads++
		if objectName != "TargetObject" {
			t.Fatalf("entry loader objectName = %q; want TargetObject", objectName)
		}
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("SourceId", "sa_createTarget", "createTarget"),
		}, nil
	})

	result, err := ps.BulkInsert(
		context.Background(),
		"TargetObject",
		[]models.SObject{{constants.FieldName: "flow-authored-source"}},
		recordEntrypointUser(),
		BulkInsertOptions{SkipValidation: true},
	)
	if err == nil {
		t.Fatal("BulkInsert err = nil; want all-records-failed validation error")
	}
	if result.SuccessCount != 0 || result.FailedCount != 1 {
		t.Fatalf("result counts = success %d failed %d; want success 0 failed 1", result.SuccessCount, result.FailedCount)
	}
	if beforeCreateCount != 1 {
		t.Fatalf("RecordBeforeCreate count = %d; want 1", beforeCreateCount)
	}
	if candidateLoads != 1 {
		t.Fatalf("candidate loader calls = %d; want 1", candidateLoads)
	}
	if entryLoads != 1 {
		t.Fatalf("entry loader calls = %d; want 1 after before-save writes SourceId", entryLoads)
	}
	if len(result.Errors) != 1 ||
		!strings.Contains(result.Errors[0], "Direct create of TargetObject") ||
		!strings.Contains(result.Errors[0], "createTarget") {
		t.Fatalf("result.Errors = %#v; want direct-create rejection naming createTarget", result.Errors)
	}
	if len(repo.bulkInserted) != 0 {
		t.Fatalf("bulkInserted = %#v; want no DML after before-save guard rejection", repo.bulkInserted)
	}
}

type retryAfterFirstSuccessTxManager struct {
	recordingBulkTxManager
	retriesLeft int
	retryErr    error
}

func (m *retryAfterFirstSuccessTxManager) WithPessimisticRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	if m.IsPessimisticTx(ctx) {
		return fn(ctx)
	}
	err := m.WithPessimisticTransaction(ctx, fn)
	if err != nil {
		return err
	}
	if m.retriesLeft > 0 {
		m.retriesLeft--
		return m.retryErr
	}
	return nil
}

func TestBulkInsertCreateFromSourceGuardReevaluatesBeforeSaveSourceFKOnRetry(t *testing.T) {
	previousClassifier := fieldprovenance.Default()
	t.Cleanup(func() { fieldprovenance.SetDefault(previousClassifier) })
	fieldprovenance.SetDefault(fieldprovenance.NewClassifier(nil, nil, nil, nil))

	siblingMaxParent := "ParentId"
	schema := &models.ObjectMetadata{
		APIName:      "TargetObject",
		Label:        "Target Object",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{APIName: "ParentId", Type: constants.FieldTypeReference, ReferenceTo: []string{"ParentObject"}, Createable: true, Updateable: true},
			{APIName: "SortOrder__c", Type: models.FieldType("integer"), Createable: true, Updateable: true, SiblingMaxParentField: &siblingMaxParent},
			{APIName: "SourceId", Type: constants.FieldTypeReference, ReferenceTo: []string{"SourceObject"}, Createable: true, Updateable: true},
		},
	}
	repo := &captureRecordRepo{old: models.SObject{constants.FieldID: "src-flow"}}
	ps := newRecordEntrypointTestService(schema, repo)
	ps.txManager = &retryAfterFirstSuccessTxManager{
		retriesLeft: 1,
		retryErr: &parentLockPrelockRetryError{
			keys:   []recordlocks.ParentKey{{ObjectName: "SourceObject", RecordID: "src-flow"}},
			reason: "test retry after before-save mutation",
		},
	}

	var beforeCreateCount int
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, record models.SObject) {
		if eventType != sharedEvents.RecordBeforeCreate {
			return
		}
		beforeCreateCount++
		if record[constants.FieldName] == "guarded-retry" {
			record["SourceId"] = "src-flow"
		}
	}}
	ps.SetGuardCandidateFieldLoader(func(_ context.Context, objectName string) ([]string, error) {
		if objectName != "TargetObject" {
			t.Fatalf("candidate loader objectName = %q; want TargetObject", objectName)
		}
		return []string{"SourceId"}, nil
	})
	ps.SetGuardEntryLoader(guardEntriesFn(
		owningEntry("SourceId", "sa_createTarget", "createTarget"),
	))

	result, err := ps.BulkInsert(
		context.Background(),
		"TargetObject",
		[]models.SObject{
			{constants.FieldName: "guarded-retry", "SortOrder__c": 1},
			{constants.FieldName: "survivor", "SortOrder__c": 2},
		},
		recordEntrypointUser(),
		BulkInsertOptions{SkipValidation: true},
	)
	if err != nil {
		t.Fatalf("BulkInsert err = %v result=%#v bulkInserted=%#v beforeCreateCount=%d; want partial success", err, result, repo.bulkInserted, beforeCreateCount)
	}
	if beforeCreateCount != 4 {
		t.Fatalf("RecordBeforeCreate count = %d; want 4 across initial attempt and retry", beforeCreateCount)
	}
	if result.SuccessCount != 1 || result.FailedCount != 1 {
		t.Fatalf("result counts = success %d failed %d; want success 1 failed 1", result.SuccessCount, result.FailedCount)
	}
	if len(result.Errors) != 1 ||
		!strings.Contains(result.Errors[0], "Direct create of TargetObject") ||
		!strings.Contains(result.Errors[0], "createTarget") {
		t.Fatalf("result.Errors = %#v; want one direct-create rejection naming createTarget", result.Errors)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulkInserted = %#v; want only the retry-safe survivor row", repo.bulkInserted)
	}
	if got := repo.bulkInserted[0][constants.FieldName]; got != "survivor" {
		t.Fatalf("bulkInserted[0].Name = %#v; want survivor", got)
	}
	if _, leaked := repo.bulkInserted[0]["SourceId"]; leaked {
		t.Fatalf("survivor row unexpectedly has SourceId after retry: %#v", repo.bulkInserted[0])
	}
}

func TestBulkInsertCreateFromSourceGuardPreservesCallerLookupPrecedence(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      "TargetObject",
		Label:        "Target Object",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{APIName: "SourceId", Type: constants.FieldTypeReference, ReferenceTo: []string{"SourceObject"}, Createable: true, Updateable: true},
		},
	}
	repo := &captureRecordRepo{}
	ps := newRecordEntrypointTestService(schema, repo)
	var entryLoads int
	ps.SetGuardCandidateFieldLoader(func(_ context.Context, objectName string) ([]string, error) {
		if objectName != "TargetObject" {
			t.Fatalf("candidate loader objectName = %q; want TargetObject", objectName)
		}
		return []string{"SourceId"}, nil
	})
	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		entryLoads++
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("SourceId", "sa_createTarget", "createTarget"),
		}, nil
	})

	result, err := ps.BulkInsert(
		context.Background(),
		"TargetObject",
		[]models.SObject{{
			constants.FieldName: "missing source",
			"SourceId":          "missing-src",
		}},
		recordEntrypointUser(),
		BulkInsertOptions{},
	)
	if err == nil {
		t.Fatal("BulkInsert err = nil; want all-records-failed validation error")
	}
	if result.SuccessCount != 0 || result.FailedCount != 1 {
		t.Fatalf("result counts = success %d failed %d; want success 0 failed 1", result.SuccessCount, result.FailedCount)
	}
	if entryLoads != 0 {
		t.Fatalf("guard entry loader calls = %d; want lookup validation to fail before semantic guard load", entryLoads)
	}
	if len(result.Errors) != 1 ||
		!strings.Contains(result.Errors[0], `SourceId: referenced SourceObject record "missing-src" is not accessible or does not exist`) {
		t.Fatalf("result.Errors = %#v; want SourceId lookup validation error", result.Errors)
	}
	if strings.Contains(result.Errors[0], "Direct create of TargetObject") ||
		strings.Contains(result.Errors[0], "createTarget") {
		t.Fatalf("result.Errors = %#v; lookup validation must win over create-from-source guard", result.Errors)
	}
	if len(repo.bulkInserted) != 0 {
		t.Fatalf("bulkInserted = %#v; want no DML after lookup validation failure", repo.bulkInserted)
	}
}

func TestBulkInsertCreateFromSourceGuardIgnoresDefaultedSourceFK(t *testing.T) {
	defaultSourceID := "src-default"
	schema := &models.ObjectMetadata{
		APIName:      "TargetObject",
		Label:        "Target Object",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{
				APIName:      "SourceId",
				Type:         constants.FieldTypeReference,
				ReferenceTo:  []string{"SourceObject"},
				Createable:   true,
				Updateable:   true,
				DefaultValue: &defaultSourceID,
			},
		},
	}
	repo := &captureRecordRepo{}
	ps := newRecordEntrypointTestService(schema, repo)
	candidateLoads := 0
	entryLoads := 0
	ps.SetGuardCandidateFieldLoader(func(_ context.Context, objectName string) ([]string, error) {
		candidateLoads++
		if objectName != "TargetObject" {
			t.Fatalf("candidate loader objectName = %q; want TargetObject", objectName)
		}
		return []string{"SourceId"}, nil
	})
	ps.SetGuardEntryLoader(func(_ context.Context, _ string) ([]recordsguard.CreateFromSourceGuardEntry, error) {
		entryLoads++
		return []recordsguard.CreateFromSourceGuardEntry{
			owningEntry("SourceId", "sa_createTarget", "createTarget"),
		}, nil
	})

	result, err := ps.BulkInsert(
		context.Background(),
		"TargetObject",
		[]models.SObject{{constants.FieldName: "plain"}},
		recordEntrypointUser(),
		BulkInsertOptions{SkipValidation: true},
	)
	if err != nil {
		t.Fatalf("BulkInsert err = %v; want nil", err)
	}
	if result.SuccessCount != 1 || result.FailedCount != 0 {
		t.Fatalf("result counts = success %d failed %d; want success 1 failed 0", result.SuccessCount, result.FailedCount)
	}
	if candidateLoads != 1 {
		t.Fatalf("candidate loader calls = %d; want 1", candidateLoads)
	}
	if entryLoads != 0 {
		t.Fatalf("entry loader calls = %d; want 0 because caller did not set SourceId", entryLoads)
	}
	if len(repo.bulkInserted) != 1 {
		t.Fatalf("bulkInserted = %d rows; want 1", len(repo.bulkInserted))
	}
	if got := repo.bulkInserted[0]["SourceId"]; got != defaultSourceID {
		t.Fatalf("bulkInserted SourceId = %#v; want schema default %q", got, defaultSourceID)
	}
}
