package permissions

import (
	"context"
	"sync/atomic"
	"testing"

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

// stringPtr is a small test helper for declaring optional formula fields.
func stringPtr(s string) *string { return &s }

func TestEvaluateLockRule(t *testing.T) {
	ps := &PermissionService{formula: formula.NewEngine()}

	tests := []struct {
		name   string
		schema *models.ObjectMetadata
		record models.SObject
		want   bool
	}{
		{
			name:   "nil schema yields unlocked",
			schema: nil,
			record: models.SObject{},
			want:   false,
		},
		{
			name:   "no LockRule yields unlocked",
			schema: &models.ObjectMetadata{APIName: "Sample"},
			record: models.SObject{},
			want:   false,
		},
		{
			name:   "empty LockRule yields unlocked",
			schema: &models.ObjectMetadata{APIName: "Sample", LockRule: stringPtr("")},
			record: models.SObject{},
			want:   false,
		},
		{
			name:   "whitespace LockRule yields unlocked",
			schema: &models.ObjectMetadata{APIName: "Sample", LockRule: stringPtr("   ")},
			record: models.SObject{},
			want:   false,
		},
		{
			name:   "expression evaluating true yields locked",
			schema: &models.ObjectMetadata{APIName: "Sample", LockRule: stringPtr("Status == 'Closed'")},
			record: models.SObject{"Status": "Closed"},
			want:   true,
		},
		{
			name:   "expression evaluating false yields unlocked",
			schema: &models.ObjectMetadata{APIName: "Sample", LockRule: stringPtr("Status == 'Closed'")},
			record: models.SObject{"Status": "Open"},
			want:   false,
		},
		{
			name:   "malformed expression fails open (unlocked) rather than hiding mutation UI",
			schema: &models.ObjectMetadata{APIName: "Sample", LockRule: stringPtr("not a formula )")},
			record: models.SObject{},
			want:   false,
		},
	}

	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := ps.evaluateLockRule(tc.schema, tc.record, user)
			if got != tc.want {
				t.Fatalf("evaluateLockRule() = %v, want %v", got, tc.want)
			}
		})
	}
}

// TestEvaluateLockRule_NoEngine asserts the helper degrades gracefully when
// the PermissionService is constructed without a formula engine (legacy paths /
// minimal test fixtures). It must not panic or report records as locked.
func TestEvaluateLockRule_NoEngine(t *testing.T) {
	ps := &PermissionService{formula: nil}
	schema := &models.ObjectMetadata{APIName: "Sample", LockRule: stringPtr("Status == 'Closed'")}
	record := models.SObject{"Status": "Closed"}
	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	if ps.evaluateLockRule(schema, record, user) {
		t.Fatalf("expected unlocked when formula engine is nil")
	}
}

// TestCheckRecordAccess_LockRuleEnforcedAtMutationGate is the regression
// test for the CRITICAL Codex blocker: LockRule must be enforced in the
// backend mutation path (checkRecordAccessInternal), not only reflected in the
// UI-facing IsLocked flag.
//
// Schema has SharingModel=PublicReadWrite so ownership/sharing checks are not the
// focus here — the assertion is purely on the LockRule gate.
// flsFallbackRepo (from service_fls_fallback_test.go) provides the minimal repo
// stubs needed: LoadEffectiveObjectPermission (nil), GetEffectiveSystemPermissions
// (empty), and GetPendingApprovalLockInfo (no lock) via its embedded saveTestRepo.
func TestCheckRecordAccess_LockRuleEnforcedAtMutationGate(t *testing.T) {
	repo := &flsFallbackRepo{}
	ps := &PermissionService{
		formula: formula.NewEngine(),
		repo:    repo,
	}
	condTrue := "Status == 'Closed'"
	schema := &models.ObjectMetadata{
		APIName:      "Sample",
		SharingModel: constants.SharingModelPublicReadWrite,
		LockRule:     stringPtr(condTrue),
	}
	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	record := models.SObject{"Id": "rec-1", constants.FieldOwnerID: "user-1", "Status": "Closed"}
	ctx := context.Background()

	// Read must be allowed even when LockRule is true.
	if !ps.CheckRecordAccess(ctx, schema, record, constants.PermRead, user) {
		t.Fatal("CheckRecordAccess(PermRead) must return true even when LockRule is true")
	}
	// Edit must be denied when LockRule is true.
	if ps.CheckRecordAccess(ctx, schema, record, constants.PermEdit, user) {
		t.Fatal("CheckRecordAccess(PermEdit) must return false when LockRule evaluates true")
	}
	// Delete must be denied when LockRule is true.
	if ps.CheckRecordAccess(ctx, schema, record, constants.PermDelete, user) {
		t.Fatal("CheckRecordAccess(PermDelete) must return false when LockRule evaluates true")
	}

	// Now with condition false: mutation must be allowed (PublicReadWrite OWD).
	recordOpen := models.SObject{"Id": "rec-2", constants.FieldOwnerID: "user-1", "Status": "Open"}
	if !ps.CheckRecordAccess(ctx, schema, recordOpen, constants.PermEdit, user) {
		t.Fatal("CheckRecordAccess(PermEdit) must return true when LockRule evaluates false")
	}
}

// TestEvaluateLockRule_NoCRMFieldNamesLeak is a structural guard: the
// engine evaluates a generic expression against an arbitrary record. The L0
// binary must not encode any specific application field name. We verify by
// running the same evaluator across application-shaped surrogates (a generic
// work-item record vs a "CRM Order"-style record) using semantically equivalent
// formulas — both must produce the locked result purely from the
// expression+record, with the platform code unchanged.
func TestEvaluateLockRule_AppNeutral(t *testing.T) {
	ps := &PermissionService{formula: formula.NewEngine()}

	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}

	// Generic work-item shape
	issueSchema := &models.ObjectMetadata{
		APIName:  "Issue__c",
		LockRule: stringPtr("Resolution == 'Done'"),
	}
	issueRec := models.SObject{"Resolution": "Done"}
	if !ps.evaluateLockRule(issueSchema, issueRec, user) {
		t.Fatalf("expected Issue with Resolution=Done to be locked")
	}

	// Order shape (different vocabulary, same evaluator)
	orderSchema := &models.ObjectMetadata{
		APIName:  "Order",
		LockRule: stringPtr("Status == 'Activated'"),
	}
	orderRec := models.SObject{"Status": "Activated"}
	if !ps.evaluateLockRule(orderSchema, orderRec, user) {
		t.Fatalf("expected Order with Status=Activated to be locked")
	}
}

// countingLockRepo wraps saveTestRepo and counts GetPendingApprovalLockInfo calls.
// Used to assert the double-probe fix in the batch path (HIGH 1).
type countingLockRepo struct {
	saveTestRepo
	lockInfoCalls atomic.Int64
	// lockedRecordID, when non-empty, makes GetPendingApprovalLockInfo return a
	// workitem-type lock for that record so isApprovalLockedForOperation returns true.
	lockedRecordID string
}

func (r *countingLockRepo) GetPendingApprovalLockInfo(_ context.Context, recordID string) (string, string, error) {
	r.lockInfoCalls.Add(1)
	if r.lockedRecordID != "" && recordID == r.lockedRecordID {
		return "proc-1", "admin", nil
	}
	return "", "", nil
}

// TestBatchCheckRecordAccess_SingleProbePerRecord asserts that BatchCheckRecordAccess
// calls GetPendingApprovalLockInfo exactly once per record after the HIGH 1 fix.
// Before the fix, unlocked records triggered 3 probes (prefetch + 2 inside
// checkRecordAccessInternal for PermEdit and PermDelete).
func TestBatchCheckRecordAccess_SingleProbePerRecord(t *testing.T) {
	repo := &countingLockRepo{}
	ps := &PermissionService{
		formula: formula.NewEngine(),
		repo:    repo,
	}
	schema := &models.ObjectMetadata{
		APIName:      "Sample",
		SharingModel: constants.SharingModelPublicReadWrite,
	}
	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	records := []models.SObject{
		{"Id": "rec-1", constants.FieldOwnerID: "user-1"},
		{"Id": "rec-2", constants.FieldOwnerID: "user-1"},
	}
	ctx := context.Background()

	_ = ps.BatchCheckRecordAccess(ctx, schema, records, user)

	got := repo.lockInfoCalls.Load()
	// Exactly 1 probe per record (one each for the PermEdit prefetch in BatchCheckRecordAccess).
	// PermRead bypasses the lock; PermEdit and PermDelete reuse the prefetched value.
	if got != int64(len(records)) {
		t.Fatalf("GetPendingApprovalLockInfo call count = %d, want %d (one per record)", got, len(records))
	}
}

// TestComputeIsLockedWith_LockRuleBypassSemantics asserts the split-bypass
// rule introduced when canBypassLockRule was separated from
// canBypassApprovalLock: ServiceContext+ bypasses LockRule, but super-user
// capability holders do NOT — admin-authored LockRule binds all non-service
// callers.
func TestComputeIsLockedWith_LockRuleBypassSemantics(t *testing.T) {
	ps := &PermissionService{formula: formula.NewEngine()}
	schema := &models.ObjectMetadata{
		APIName:  "Sample",
		LockRule: stringPtr("Status == 'Closed'"),
	}
	record := models.SObject{"Status": "Closed"}

	// Regular user: LockRule true → isLocked true.
	regularUser := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	if !ps.computeIsLockedWith(context.Background(), schema, record, regularUser, false) {
		t.Fatal("regular user with LockRule=true must see isLocked=true")
	}

	// Super-user capability holder: LockRule true -> isLocked TRUE
	// (admin-authored invariant binds even admins).
	superUser := &models.UserSession{ID: "admin-1", ProfileID: "jira_admin"}
	if !ps.computeIsLockedWith(context.Background(), schema, record, superUser, false) {
		t.Fatal("super-user capability holder with LockRule=true must see isLocked=true (LockRule binds admins)")
	}

	// Legacy admin flag: same — no bypass.
	sysAdminUser := &models.UserSession{ID: "admin-2", ProfileID: "standard", IsSystemAdmin: true}
	if !ps.computeIsLockedWith(context.Background(), schema, record, sysAdminUser, false) {
		t.Fatal("IsSystemAdmin user with LockRule=true must see isLocked=true (LockRule binds admins)")
	}

	// ServiceContext: still bypasses LockRule (system-internal callers must be
	// able to mutate records without tripping admin-authored business invariants).
	svcCtx := constants.WithSecurityContext(context.Background(), constants.ServiceContext, "test")
	if ps.computeIsLockedWith(svcCtx, schema, record, regularUser, false) {
		t.Fatal("ServiceContext with LockRule=true must see isLocked=false (ServiceContext bypass)")
	}
	// ServiceContext also bypasses for admin users.
	if ps.computeIsLockedWith(svcCtx, schema, record, sysAdminUser, false) {
		t.Fatal("ServiceContext+admin with LockRule=true must see isLocked=false (ServiceContext bypass)")
	}

	// approvalLocked=true overrides everything: always locked, even for admins.
	if !ps.computeIsLockedWith(context.Background(), schema, record, superUser, true) {
		t.Fatal("approvalLocked=true must still report isLocked=true even for SuperUser")
	}
}

// TestEvaluateLockRuleForGate_SessionGlobals asserts that $Profile.Name and
// $User session globals are available to LockRule formulas after the HIGH 1 fix.
// Before the fix, evaluateLockRuleForGate omitted User from formula.Context, so
// expressions like "$Profile.Name == 'Sales'" would error at runtime and fail closed —
// preventing all edits while the UI path failed open (unlocked).
func TestEvaluateLockRuleForGate_SessionGlobals(t *testing.T) {
	ps := &PermissionService{formula: formula.NewEngine()}

	salesProfile := "Sales"
	schema := &models.ObjectMetadata{
		APIName:  "Sample",
		LockRule: stringPtr("$Profile.Name == 'Sales'"),
	}
	record := models.SObject{"Id": "rec-1"}

	// User whose profile name is "Sales": formula should evaluate to true → locked.
	salesUser := &models.UserSession{ID: "u-1", ProfileID: "prof-sales", ProfileName: salesProfile}
	locked, err := ps.evaluateLockRuleForGate(schema, record, salesUser)
	if err != nil {
		t.Fatalf("evaluateLockRuleForGate with $Profile.Name: unexpected error: %v", err)
	}
	if !locked {
		t.Fatal("expected locked=true when $Profile.Name == 'Sales' and user profile is 'Sales'")
	}

	// User whose profile name is "Support": formula should evaluate to false → not locked.
	supportUser := &models.UserSession{ID: "u-2", ProfileID: "prof-support", ProfileName: "Support"}
	locked, err = ps.evaluateLockRuleForGate(schema, record, supportUser)
	if err != nil {
		t.Fatalf("evaluateLockRuleForGate with $Profile.Name: unexpected error for support user: %v", err)
	}
	if locked {
		t.Fatal("expected locked=false when $Profile.Name == 'Sales' but user profile is 'Support'")
	}
}

// TestIsRecordLockedForGate_FailClosed verifies the fail-closed semantics of
// IsRecordLockedForGate: a malformed LockRule formula that errors at evaluation
// time must return true (locked), not false (unlocked / fail-open).
func TestIsRecordLockedForGate_FailClosed(t *testing.T) {
	repo := &flsFallbackRepo{}
	ps := &PermissionService{
		formula: formula.NewEngine(),
		repo:    repo,
	}
	// A syntactically invalid expression will cause the formula engine to error.
	schema := &models.ObjectMetadata{
		APIName:  "Sample",
		LockRule: stringPtr("not a formula )"),
	}
	record := models.SObject{"Id": "rec-1"}
	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}

	// IsRecordLockedForGate must return true (fail-closed) when evaluation errors.
	if !ps.IsRecordLockedForGate(context.Background(), schema, record, user) {
		t.Fatal("IsRecordLockedForGate must return true (fail-closed) when formula evaluation errors")
	}
}

// TestIsRecordLockedForGate_NormalPath verifies that IsRecordLockedForGate
// returns correct lock/unlock decisions for well-formed formulas.
func TestIsRecordLockedForGate_NormalPath(t *testing.T) {
	repo := &flsFallbackRepo{}
	ps := &PermissionService{
		formula: formula.NewEngine(),
		repo:    repo,
	}
	schema := &models.ObjectMetadata{
		APIName:  "Sample",
		LockRule: stringPtr("Status == 'Closed'"),
	}
	user := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	ctx := context.Background()

	// Formula true → locked.
	lockedRecord := models.SObject{"Id": "rec-1", "Status": "Closed"}
	if !ps.IsRecordLockedForGate(ctx, schema, lockedRecord, user) {
		t.Fatal("IsRecordLockedForGate must return true when LockRule evaluates true")
	}

	// Formula false → not locked.
	openRecord := models.SObject{"Id": "rec-2", "Status": "Open"}
	if ps.IsRecordLockedForGate(ctx, schema, openRecord, user) {
		t.Fatal("IsRecordLockedForGate must return false when LockRule evaluates false")
	}
}

// TestIsRecordLockedForGate_LockRuleBypassSemantics verifies the split-bypass
// rule in IsRecordLockedForGate (matches checkRecordAccessInternal): only
// ServiceContext+ bypasses LockRule. System administrators and SuperUsers are
// subject to admin-authored LockRule by design.
func TestIsRecordLockedForGate_LockRuleBypassSemantics(t *testing.T) {
	repo := &flsFallbackRepo{}
	ps := &PermissionService{
		formula: formula.NewEngine(),
		repo:    repo,
	}
	schema := &models.ObjectMetadata{
		APIName:  "Sample",
		LockRule: stringPtr("Status == 'Closed'"),
	}
	record := models.SObject{"Id": "rec-1", "Status": "Closed"}
	regularUser := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	sysAdminUser := &models.UserSession{ID: "admin-1", ProfileID: "jira_admin"}
	isSysAdminUser := &models.UserSession{ID: "admin-2", ProfileID: "standard", IsSystemAdmin: true}
	svcCtx := constants.WithSecurityContext(context.Background(), constants.ServiceContext, "test")

	// Regular user: LockRule true → locked.
	if !ps.IsRecordLockedForGate(context.Background(), schema, record, regularUser) {
		t.Fatal("regular user: IsRecordLockedForGate must return true when LockRule=true")
	}
	// Super-user capability holder: LockRule still binds -> locked.
	if !ps.IsRecordLockedForGate(context.Background(), schema, record, sysAdminUser) {
		t.Fatal("super-user capability holder: IsRecordLockedForGate must return true when LockRule=true (LockRule binds admins)")
	}
	// IsSystemAdmin flag: same — LockRule still binds.
	if !ps.IsRecordLockedForGate(context.Background(), schema, record, isSysAdminUser) {
		t.Fatal("IsSystemAdmin user: IsRecordLockedForGate must return true when LockRule=true (LockRule binds admins)")
	}
	// ServiceContext: still bypasses LockRule → not locked.
	if ps.IsRecordLockedForGate(svcCtx, schema, record, regularUser) {
		t.Fatal("ServiceContext: IsRecordLockedForGate must return false (ServiceContext bypass)")
	}
	// ServiceContext bypass holds even when paired with admin user.
	if ps.IsRecordLockedForGate(svcCtx, schema, record, sysAdminUser) {
		t.Fatal("ServiceContext+admin: IsRecordLockedForGate must return false (ServiceContext bypass)")
	}
}

// TestCheckRecordAccess_AdminNoLongerBypassesLockRule asserts that super-user
// capability holders do NOT bypass an active LockRule via
// checkRecordAccessInternal — the capability short-circuit runs AFTER the
// LockRule gate.
func TestCheckRecordAccess_AdminNoLongerBypassesLockRule(t *testing.T) {
	repo := &flsFallbackRepo{systemPerms: superUserCapabilityGrant()}
	ps := &PermissionService{
		formula: formula.NewEngine(),
		repo:    repo,
	}
	schema := &models.ObjectMetadata{
		APIName:      "Sample",
		SharingModel: constants.SharingModelPublicReadWrite,
		LockRule:     stringPtr("Status == 'Closed'"),
	}
	record := models.SObject{"Id": "rec-1", constants.FieldOwnerID: "anyone", "Status": "Closed"}
	ctx := context.Background()

	// Super-user capability holder: Edit on locked record must be denied.
	superUser := &models.UserSession{ID: "admin-1", ProfileID: "jira_admin"}
	if ps.CheckRecordAccess(ctx, schema, record, constants.PermEdit, superUser) {
		t.Fatal("super-user capability holder: CheckRecordAccess(PermEdit) must return false when LockRule=true (admins bound by LockRule)")
	}
	// Super-user capability holders must still be able to READ — LockRule does not block reads.
	if !ps.CheckRecordAccess(ctx, schema, record, constants.PermRead, superUser) {
		t.Fatal("super-user capability holder: CheckRecordAccess(PermRead) must return true even when LockRule=true (LockRule is mutation-only)")
	}

	// ServiceContext: still bypasses → write allowed.
	svcCtx := constants.WithSecurityContext(ctx, constants.ServiceContext, "test")
	regularUser := &models.UserSession{ID: "user-1", ProfileID: "standard"}
	if !ps.CheckRecordAccess(svcCtx, schema, record, constants.PermEdit, regularUser) {
		t.Fatal("ServiceContext: CheckRecordAccess(PermEdit) must return true (ServiceContext bypasses LockRule)")
	}
}
