package records

import (
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

const guardedUpdateLifecycleCapability = "test.guarded-update-lifecycle"

type divergentGuardedUpdateRepo struct {
	prelockUpdateParentsRepo
	preflight models.SObject
	locked    models.SObject
}

func (r *divergentGuardedUpdateRepo) FindOne(context.Context, string, string) (models.SObject, error) {
	return cloneSObject(r.preflight), nil
}

func (r *divergentGuardedUpdateRepo) GetLock(_ context.Context, tableName, id string) (models.SObject, error) {
	r.lockCalls = append(r.lockCalls, tableName+":"+id)
	return cloneSObject(r.locked), nil
}

func guardedUpdateSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      "GuardedRecord__c",
		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: "Inactive__c", Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
		},
	}
	schema.AccessPolicy = &models.ObjectAccessPolicy{
		Lifecycle: &models.RecordLifecycleAccessPolicy{
			Hooks: []models.RecordLifecycleHookPolicy{{
				Phase:      string(contract.RecordLifecycleAfterUpdateInTx),
				Capability: guardedUpdateLifecycleCapability,
			}},
		},
	}
	makeFieldsMap(schema)
	return schema
}

func newGuardedUpdateTestService(t *testing.T, repo *prelockUpdateParentsRepo, eventCount *int, lifecycleCount *int) *PersistenceService {
	t.Helper()
	ps := newRollupBoundaryTestService(repo, guardedUpdateSchema())
	ps.eventBus = mutatingRecordEventBus{mutate: func(eventType sharedEvents.EventType, _ models.SObject) {
		if eventType == sharedEvents.RecordBeforeUpdate || eventType == sharedEvents.RecordAfterUpdate {
			*eventCount++
		}
	}}
	if err := ps.RegisterRecordLifecycleHook(contract.RecordLifecycleRegistration{
		Capability: guardedUpdateLifecycleCapability,
		Phase:      contract.RecordLifecycleAfterUpdateInTx,
		Hook: func(context.Context, contract.RecordLifecycleEvent) error {
			*lifecycleCount++
			return nil
		},
	}); err != nil {
		t.Fatalf("RegisterRecordLifecycleHook: %v", err)
	}
	return ps
}

func guardedUpdateRows(inactive bool) map[string]models.SObject {
	return map[string]models.SObject{
		"GuardedRecord__c/row-1": {
			constants.FieldID:   "row-1",
			constants.FieldName: "before",
			"Inactive__c":       inactive,
		},
	}
}

func TestUpdateGuardedEvaluatesLockedRowAfterParentPrelock(t *testing.T) {
	tests := []struct {
		name        string
		inactive    bool
		wantMatched bool
	}{
		{name: "locked row no longer matches", inactive: true},
		{name: "locked row still matches", wantMatched: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			updateCalls := 0
			repo := &prelockUpdateParentsRepo{
				stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(context.Context, string, string, models.SObject) error {
					updateCalls++
					return nil
				}},
				rows: guardedUpdateRows(tt.inactive),
			}
			eventCount := 0
			lifecycleCount := 0
			ps := newGuardedUpdateTestService(t, repo, &eventCount, &lifecycleCount)
			guardCalls := 0

			matched, err := ps.UpdateGuarded(context.Background(), "GuardedRecord__c", "row-1", models.SObject{
				constants.FieldName: "guarded write",
			}, recordEntrypointUser(), func(_ context.Context, locked models.SObject) (bool, error) {
				guardCalls++
				wantLocks := []string{"GuardedRecord__c:row-1"}
				if !reflect.DeepEqual(repo.lockCalls, wantLocks) {
					t.Fatalf("locks visible at guard = %#v, want locked child %#v", repo.lockCalls, wantLocks)
				}
				inactive, _ := locked["Inactive__c"].(bool)
				return !inactive, nil
			})
			if err != nil {
				t.Fatalf("UpdateGuarded: %v", err)
			}
			if matched != tt.wantMatched {
				t.Fatalf("matched = %v, want %v", matched, tt.wantMatched)
			}
			if guardCalls != 1 {
				t.Fatalf("guard calls = %d, want 1", guardCalls)
			}
			wantLifecycle := 0
			wantEvents := 0
			wantUpdates := 0
			if tt.wantMatched {
				wantLifecycle = 1
				wantEvents = 1
				wantUpdates = 1
			}
			if updateCalls != wantUpdates || eventCount != wantEvents || lifecycleCount != wantLifecycle {
				t.Fatalf("update/event/lifecycle calls = %d/%d/%d, want %d/%d/%d", updateCalls, eventCount, lifecycleCount, wantUpdates, wantEvents, wantLifecycle)
			}
		})
	}
}

func TestNestedUpdateGuardedCannotBorrowOuterAdoptionProvenance(t *testing.T) {
	const ownerField = "OwnerIdentity__c"
	duplicate := errors.New("nested guarded update duplicate")
	schema := guardedUpdateSchema()
	schema.AccessPolicy.Lifecycle = nil
	schema.Fields = append(schema.Fields, models.FieldMetadata{
		APIName: ownerField, Type: constants.FieldTypeString, IsUnique: true, Createable: true, Updateable: true,
	})
	makeFieldsMap(schema)
	updateCalls := 0
	repo := &prelockUpdateParentsRepo{
		stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(context.Context, string, string, models.SObject) error {
			updateCalls++
			return duplicate
		}},
		rows: map[string]models.SObject{
			"GuardedRecord__c/row-1": {
				constants.FieldID:   "row-1",
				constants.FieldName: "before",
				ownerField:          "outer-owner",
			},
		},
	}
	ps := newRollupBoundaryTestService(repo, schema)
	guardCtx, outerGuard := withExternalIDUpsertAdoptionGuard(
		context.Background(),
		"GuardedRecord__c",
		"row-1",
		[]string{ownerField},
	)
	outerCtx := enterExternalIDUpsertAdoptionGuardUpdate(guardCtx, "GuardedRecord__c", "row-1")

	matched, err := ps.UpdateGuarded(
		outerCtx,
		"GuardedRecord__c",
		"row-1",
		models.SObject{ownerField: "nested-owner"},
		recordEntrypointUser(),
		func(context.Context, models.SObject) (bool, error) { return true, nil },
	)
	if !errors.Is(err, duplicate) {
		t.Fatalf("nested UpdateGuarded error = %v, want repository duplicate", err)
	}
	if !matched || updateCalls != 1 {
		t.Fatalf("nested UpdateGuarded matched/update calls = %v/%d, want true/1", matched, updateCalls)
	}
	if externalIDUpsertAdoptionGuardDirectMutationConflict(err, outerGuard) {
		t.Fatalf("nested UpdateGuarded repository conflict borrowed outer adoption provenance: %v", err)
	}
}

func TestUpdateGuardedRechecksManagedOwnershipFromLockedImage(t *testing.T) {
	updateCalls := 0
	repo := &divergentGuardedUpdateRepo{
		prelockUpdateParentsRepo: prelockUpdateParentsRepo{
			stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(context.Context, string, string, models.SObject) error {
				updateCalls++
				return nil
			}},
		},
		preflight: models.SObject{
			constants.FieldID:                         "template-1",
			constants.FieldSysEmailTemplate_OwnerPack: "",
		},
		locked: models.SObject{
			constants.FieldID:                         "template-1",
			constants.FieldSysEmailTemplate_OwnerPack: "crm",
		},
	}
	schema := &models.ObjectMetadata{
		APIName:      constants.TableEmailTemplate,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString},
			{APIName: constants.FieldSysEmailTemplate_Subject, Type: constants.FieldTypeString, Updateable: true},
			{APIName: constants.FieldSysEmailTemplate_OwnerPack, Type: constants.FieldTypeString},
		},
	}
	makeFieldsMap(schema)
	ps := newRollupBoundaryTestService(repo, schema)

	matched, err := ps.UpdateGuarded(context.Background(), constants.TableEmailTemplate, "template-1", models.SObject{
		constants.FieldSysEmailTemplate_Subject: "changed",
	}, recordEntrypointUser(), func(context.Context, models.SObject) (bool, error) {
		return true, nil
	})
	if err == nil || !strings.Contains(err.Error(), "owned by pack") {
		t.Fatalf("UpdateGuarded error = %v, want locked pack-ownership rejection", err)
	}
	if !matched || updateCalls != 0 {
		t.Fatalf("UpdateGuarded matched/update calls = %v/%d, want true/0", matched, updateCalls)
	}
}

func TestUpdateGuardedObservesParentThenChildLockOrder(t *testing.T) {
	repo := &prelockUpdateParentsRepo{rows: map[string]models.SObject{
		"Parent__c/p-1": {constants.FieldID: "p-1", constants.FieldName: "Parent 1"},
		"Child__c/c-1": {
			constants.FieldID:   "c-1",
			constants.FieldName: "Child 1",
			"DisplayName__c":    "before",
			"Parent__c":         "p-1",
		},
	}}
	ps := newRollupBoundaryTestService(repo, heldParentSchema(), heldParentChildSchema())
	ps.txManager = &recordingBulkTxManager{}

	matched := false
	err := ps.runGroupMembershipAwarePessimisticTransactionForObjects(context.Background(), []string{"Child__c"}, func(txCtx context.Context) error {
		var updateErr error
		matched, updateErr = ps.UpdateGuarded(txCtx, "Child__c", "c-1", models.SObject{
			"DisplayName__c": "after",
		}, recordEntrypointUser(), func(context.Context, models.SObject) (bool, error) {
			wantLocks := []string{"Parent__c:p-1", "Child__c:c-1"}
			if !reflect.DeepEqual(repo.lockCalls, wantLocks) {
				t.Fatalf("locks visible at guard = %#v, want parent then child %#v", repo.lockCalls, wantLocks)
			}
			return true, nil
		})
		return updateErr
	})
	if err != nil {
		t.Fatalf("UpdateGuarded: %v", err)
	}
	if !matched {
		t.Fatal("matched = false, want true")
	}
}

func TestUpdateGuardedErrorRollsBackBeforeLifecycle(t *testing.T) {
	sentinel := errors.New("guard evaluation failed")
	updateCalls := 0
	repo := &prelockUpdateParentsRepo{
		stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(context.Context, string, string, models.SObject) error {
			updateCalls++
			return nil
		}},
		rows: guardedUpdateRows(false),
	}
	eventCount := 0
	lifecycleCount := 0
	ps := newGuardedUpdateTestService(t, repo, &eventCount, &lifecycleCount)
	var transactionErr error
	ps.txManager = &recordingBulkTxManager{onEnd: func(_ interface{}, err error) {
		transactionErr = err
	}}

	matched, err := ps.UpdateGuarded(context.Background(), "GuardedRecord__c", "row-1", models.SObject{
		constants.FieldName: "must not write",
	}, recordEntrypointUser(), func(context.Context, models.SObject) (bool, error) {
		return false, sentinel
	})
	if !errors.Is(err, sentinel) {
		t.Fatalf("UpdateGuarded error = %v, want %v", err, sentinel)
	}
	if matched {
		t.Fatal("matched = true, want false on guard error")
	}
	if !errors.Is(transactionErr, sentinel) {
		t.Fatalf("transaction completion error = %v, want rollback receipt %v", transactionErr, sentinel)
	}
	if updateCalls != 0 || eventCount != 0 || lifecycleCount != 0 {
		t.Fatalf("update/event/lifecycle calls = %d/%d/%d, want all zero", updateCalls, eventCount, lifecycleCount)
	}
}

func TestUpdateGuardedFalseSkipsLockedStageCanonicalizers(t *testing.T) {
	const capability = "test.guarded-update-before-canonicalizer"
	sentinel := errors.New("before-update canonicalizer must not run")
	schema := guardedUpdateSchema()
	schema.AccessPolicy.WriteValidation = &models.RecordWriteValidationAccessPolicy{
		Canonicalizers: []models.RecordWriteCanonicalizationPolicy{{
			Phase:      string(contract.RecordWriteValidationBeforeUpdate),
			Capability: capability,
		}},
	}
	updateCalls := 0
	repo := &prelockUpdateParentsRepo{
		stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(context.Context, string, string, models.SObject) error {
			updateCalls++
			return nil
		}},
		rows: guardedUpdateRows(false),
	}
	ps := newRollupBoundaryTestService(repo, schema)
	preflightCanonicalizerCalls := 0
	lockedCanonicalizerCalls := 0
	if err := ps.RegisterRecordWriteCanonicalizer(contract.RecordWriteCanonicalizerRegistration{
		Capability: capability,
		Phase:      contract.RecordWriteValidationBeforeUpdate,
		Canonicalizer: func(_ context.Context, event contract.RecordWriteCanonicalizationEvent) error {
			if event.RecordID == "" {
				preflightCanonicalizerCalls++
				return nil
			}
			lockedCanonicalizerCalls++
			return sentinel
		},
	}); err != nil {
		t.Fatalf("RegisterRecordWriteCanonicalizer: %v", err)
	}

	matched, err := ps.UpdateGuarded(context.Background(), "GuardedRecord__c", "row-1", models.SObject{
		constants.FieldName: "must not canonicalize",
	}, recordEntrypointUser(), func(context.Context, models.SObject) (bool, error) {
		return false, nil
	})
	if err != nil {
		t.Fatalf("UpdateGuarded error = %v, want nil", err)
	}
	if matched {
		t.Fatal("matched = true, want false")
	}
	if preflightCanonicalizerCalls != 1 || lockedCanonicalizerCalls != 0 || updateCalls != 0 {
		t.Fatalf("preflight/locked canonicalizer/update calls = %d/%d/%d, want 1/0/0", preflightCanonicalizerCalls, lockedCanonicalizerCalls, updateCalls)
	}
}

func TestUpdateGuardedTruePreservesOrdinaryCanonicalizerLifecycle(t *testing.T) {
	const capability = "test.guarded-update-canonicalizer-parity"
	type result struct {
		canonicalizerCalls int
		written            models.SObject
	}
	run := func(t *testing.T, guarded bool) result {
		t.Helper()
		schema := guardedUpdateSchema()
		schema.AccessPolicy.Lifecycle = nil
		schema.AccessPolicy.WriteValidation = &models.RecordWriteValidationAccessPolicy{
			Canonicalizers: []models.RecordWriteCanonicalizationPolicy{{
				Phase:      string(contract.RecordWriteValidationBeforeUpdate),
				Capability: capability,
			}},
		}
		var got result
		repo := &prelockUpdateParentsRepo{
			stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(_ context.Context, _, _ string, record models.SObject) error {
				got.written = cloneSObject(record)
				return nil
			}},
			rows: guardedUpdateRows(false),
		}
		ps := newRollupBoundaryTestService(repo, schema)
		if err := ps.RegisterRecordWriteCanonicalizer(contract.RecordWriteCanonicalizerRegistration{
			Capability: capability,
			Phase:      contract.RecordWriteValidationBeforeUpdate,
			Canonicalizer: func(_ context.Context, event contract.RecordWriteCanonicalizationEvent) error {
				got.canonicalizerCalls++
				event.Record[constants.FieldName] = "canonicalized"
				return nil
			},
		}); err != nil {
			t.Fatalf("RegisterRecordWriteCanonicalizer: %v", err)
		}
		updates := models.SObject{constants.FieldName: "requested"}
		if guarded {
			matched, err := ps.UpdateGuarded(context.Background(), "GuardedRecord__c", "row-1", updates, recordEntrypointUser(), func(context.Context, models.SObject) (bool, error) {
				return true, nil
			})
			if err != nil || !matched {
				t.Fatalf("UpdateGuarded matched/error = %v/%v, want true/nil", matched, err)
			}
		} else if err := ps.Update(context.Background(), "GuardedRecord__c", "row-1", updates, recordEntrypointUser()); err != nil {
			t.Fatalf("Update: %v", err)
		}
		return got
	}

	ordinary := run(t, false)
	guarded := run(t, true)
	if guarded.canonicalizerCalls != ordinary.canonicalizerCalls || guarded.canonicalizerCalls == 0 {
		t.Fatalf("guarded/ordinary canonicalizer calls = %d/%d, want equal and nonzero", guarded.canonicalizerCalls, ordinary.canonicalizerCalls)
	}
	if !reflect.DeepEqual(guarded.written, ordinary.written) {
		t.Fatalf("guarded write = %#v, ordinary write = %#v", guarded.written, ordinary.written)
	}
}

func TestUpdateGuardedCanonicalizerCannotBypassPostCanonicalizationGuards(t *testing.T) {
	const capability = "test.guarded-update-protected-field"
	newService := func(t *testing.T) (*PersistenceService, *int, *int) {
		t.Helper()
		schema := &models.ObjectMetadata{
			APIName:      constants.TableProfile,
			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: constants.FieldProfile_IsSystem, Type: constants.FieldTypeBoolean, Createable: true, Updateable: true},
			},
			AccessPolicy: &models.ObjectAccessPolicy{WriteValidation: &models.RecordWriteValidationAccessPolicy{
				Canonicalizers: []models.RecordWriteCanonicalizationPolicy{{
					Phase:      string(contract.RecordWriteValidationBeforeUpdate),
					Capability: capability,
				}},
			}},
		}
		makeFieldsMap(schema)
		updateCalls := 0
		canonicalizerCalls := 0
		repo := &prelockUpdateParentsRepo{
			stubUpsertRecordRepo: stubUpsertRecordRepo{updateFn: func(context.Context, string, string, models.SObject) error {
				updateCalls++
				return nil
			}},
			rows: map[string]models.SObject{constants.TableProfile + "/row-1": {
				constants.FieldID:               "row-1",
				constants.FieldName:             "before",
				constants.FieldProfile_IsSystem: false,
			}},
		}
		ps := newRollupBoundaryTestService(repo, schema)
		if err := ps.RegisterRecordWriteCanonicalizer(contract.RecordWriteCanonicalizerRegistration{
			Capability: capability,
			Phase:      contract.RecordWriteValidationBeforeUpdate,
			Canonicalizer: func(_ context.Context, event contract.RecordWriteCanonicalizationEvent) error {
				canonicalizerCalls++
				event.Record[constants.FieldProfile_IsSystem] = true
				return nil
			},
		}); err != nil {
			t.Fatalf("RegisterRecordWriteCanonicalizer: %v", err)
		}
		return ps, &canonicalizerCalls, &updateCalls
	}

	for _, guarded := range []bool{false, true} {
		name := "ordinary"
		if guarded {
			name = "guarded"
		}
		t.Run(name, func(t *testing.T) {
			ps, canonicalizerCalls, updateCalls := newService(t)
			var err error
			if guarded {
				_, err = ps.UpdateGuarded(context.Background(), constants.TableProfile, "row-1", models.SObject{
					constants.FieldName: "requested",
				}, recordEntrypointUser(), func(context.Context, models.SObject) (bool, error) {
					return true, nil
				})
			} else {
				err = ps.Update(context.Background(), constants.TableProfile, "row-1", models.SObject{
					constants.FieldName: "requested",
				}, recordEntrypointUser())
			}
			if err == nil || !strings.Contains(err.Error(), "platform-managed guard") {
				t.Fatalf("Update error = %v, want protected guard rejection", err)
			}
			fields, message, ok := sharedErrors.InvalidFieldForWriteDetails(err)
			if !ok || !reflect.DeepEqual(fields, []string{constants.FieldProfile_IsSystem}) ||
				!strings.Contains(message, "platform-managed guard") {
				t.Fatalf("protected guard classification = (%v, %q, %t), want invalid-field-for-write", fields, message, ok)
			}
			if *canonicalizerCalls != 1 || *updateCalls != 0 {
				t.Fatalf("canonicalizer/update calls = %d/%d, want 1/0", *canonicalizerCalls, *updateCalls)
			}
		})
	}
}
