package permissions

import (
	"context"
	"errors"
	"strconv"
	"testing"
	"time"

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

func TestFieldProjectionAdmissionIsFIFOAndFinite(t *testing.T) {
	admission := newBoundedFieldProjectionAdmission(1, 2)
	occupiedRelease, err := admission.acquire(context.Background())
	if err != nil {
		t.Fatalf("occupy admission: %v", err)
	}

	type result struct {
		id      int
		release func()
		err     error
	}
	results := make(chan result, 2)
	for id := 1; id <= 2; id++ {
		go func(id int) {
			release, err := admission.acquire(context.Background())
			results <- result{id: id, release: release, err: err}
		}(id)
		deadline := time.Now().Add(time.Second)
		for func() bool { _, waiters := admission.snapshot(); return waiters != id }() && time.Now().Before(deadline) {
			time.Sleep(time.Millisecond)
		}
	}
	if _, err := admission.acquire(context.Background()); !errors.Is(err, ErrFieldPermissionProjectionOverloaded) {
		t.Fatalf("third waiter error = %v, want finite overload", err)
	}

	occupiedRelease()
	first := <-results
	if first.err != nil || first.id != 1 {
		t.Fatalf("first admitted waiter = id %d, error %v; want FIFO id 1", first.id, first.err)
	}
	first.release()
	second := <-results
	if second.err != nil || second.id != 2 {
		t.Fatalf("second admitted waiter = id %d, error %v; want FIFO id 2", second.id, second.err)
	}
	second.release()
	if active, waiters := admission.snapshot(); active != 0 || waiters != 0 {
		t.Fatalf("admission leaked after FIFO test: active=%d waiters=%d", active, waiters)
	}
}

func TestFieldProjectionAdmissionLimitsFromEnv(t *testing.T) {
	values := map[string]string{
		FieldProjectionActiveLimitEnv: "7",
		FieldProjectionWaiterLimitEnv: "11",
	}
	limits, err := FieldProjectionAdmissionLimitsFromEnv(func(name string) string { return values[name] })
	if err != nil {
		t.Fatalf("FieldProjectionAdmissionLimitsFromEnv: %v", err)
	}
	if limits.Active != 7 || limits.Waiters != 11 {
		t.Fatalf("limits = %+v, want active=7 waiters=11", limits)
	}

	values[FieldProjectionActiveLimitEnv] = "0"
	values[FieldProjectionWaiterLimitEnv] = "-1"
	limits, err = FieldProjectionAdmissionLimitsFromEnv(func(name string) string { return values[name] })
	if err == nil {
		t.Fatal("invalid limits returned nil error")
	}
	want := DefaultFieldProjectionAdmissionLimits()
	if limits != want {
		t.Fatalf("invalid limits fallback = %+v, want %+v", limits, want)
	}

	values[FieldProjectionActiveLimitEnv] = strconv.Itoa(maxBoundedFieldPermissionProjectionLimit + 1)
	values[FieldProjectionWaiterLimitEnv] = strconv.Itoa(maxBoundedFieldPermissionProjectionWaiterLimit + 1)
	limits, err = FieldProjectionAdmissionLimitsFromEnv(func(name string) string { return values[name] })
	if err == nil || limits != want {
		t.Fatalf("unsafe-large limits = %+v, error %v; want defaults and validation error", limits, err)
	}
}

func TestFieldDecisionsDistinguishDenyFromAuthorityFailure(t *testing.T) {
	repo := &boundedFieldProjectionRepo{
		saveTestRepo: &saveTestRepo{},
		objectRows: map[string][]models.FieldPermissions{
			"account": {{SobjectType: "Account", Field: "Name", PermissionsRead: false, PermissionsEdit: false}},
		},
	}
	user := &models.UserSession{ID: "user-1", ProfileID: "profile-1"}
	svc := NewPermissionService(repo, nil, nil, WithFieldProjectionAdmissionLimits(1, 0))
	ctx := WithAccessInputCache(context.Background())
	if allowed, err := svc.CheckFieldVisibilityWithUser(ctx, "Account", "Name", user); allowed || err != nil {
		t.Fatalf("real read deny = allowed %v, error %v; want false,nil", allowed, err)
	}
	if allowed, err := svc.CheckFieldEditabilityWithUser(ctx, "Account", "Name", user); allowed || err != nil {
		t.Fatalf("real edit deny = allowed %v, error %v; want false,nil", allowed, err)
	}

	occupiedRelease, err := svc.fieldProjectionAdmission.acquire(context.Background())
	if err != nil {
		t.Fatalf("occupy admission: %v", err)
	}
	defer occupiedRelease()
	overloadedCtx := WithAccessInputCache(context.Background())
	for name, check := range map[string]func() (bool, error){
		"read": func() (bool, error) {
			return svc.CheckFieldVisibilityWithUser(overloadedCtx, "Contact", "Email", user)
		},
		"edit": func() (bool, error) {
			return svc.CheckFieldEditabilityWithUser(overloadedCtx, "Contact", "Email", user)
		},
		"create": func() (bool, error) {
			return NewFieldAccessResolver(svc).checkFieldEditWithCreateFallback(overloadedCtx, "Contact", "Email", user)
		},
	} {
		t.Run(name, func(t *testing.T) {
			allowed, err := check()
			if allowed || !errors.Is(err, ErrFieldPermissionProjectionOverloaded) {
				t.Fatalf("decision = allowed %v, error %v; want false, typed overload", allowed, err)
			}
		})
	}
	if allObject, object := repo.calls(); allObject != 0 || object != 1 {
		t.Fatalf("repository calls = all-object %d, object %d; want only real-deny object load", allObject, object)
	}
	if point := repo.pointCallCount(); point != 0 {
		t.Fatalf("point fallback calls = %d, want 0", point)
	}
}

func mustCheckFieldVisibilityWithUser(t testing.TB, svc *PermissionService, ctx context.Context, objectAPIName, fieldAPIName string, user *models.UserSession) bool {
	t.Helper()
	allowed, err := svc.CheckFieldVisibilityWithUser(ctx, objectAPIName, fieldAPIName, user)
	if err != nil {
		t.Fatalf("CheckFieldVisibilityWithUser(%s.%s): %v", objectAPIName, fieldAPIName, err)
	}
	return allowed
}

func mustCheckFieldEditabilityWithUser(t testing.TB, svc *PermissionService, ctx context.Context, objectAPIName, fieldAPIName string, user *models.UserSession) bool {
	t.Helper()
	allowed, err := svc.CheckFieldEditabilityWithUser(ctx, objectAPIName, fieldAPIName, user)
	if err != nil {
		t.Fatalf("CheckFieldEditabilityWithUser(%s.%s): %v", objectAPIName, fieldAPIName, err)
	}
	return allowed
}

func mustEffectiveCreateable(t testing.TB, resolver *FieldAccessResolver, ctx context.Context, field *models.FieldMetadata, objectAPIName string, user *models.UserSession) bool {
	t.Helper()
	allowed, err := resolver.EffectiveCreateable(ctx, field, objectAPIName, user)
	if err != nil {
		t.Fatalf("EffectiveCreateable(%s.%s): %v", objectAPIName, field.APIName, err)
	}
	return allowed
}
