package permissions

import (
	"context"
	"errors"
	"fmt"
	"testing"

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

type contractPolicyFanoutRepo struct {
	*saveTestRepo
	grants     map[string]*models.ObjectPermissions
	bulkGrants map[string]*models.ObjectPermissions
	bulkErr    error
	pointLoads int
	bulkLoads  int
}

func (r *contractPolicyFanoutRepo) LoadEffectiveObjectPermission(_ context.Context, _ *models.UserSession, objectName string) (*models.ObjectPermissions, error) {
	r.pointLoads++
	return r.grants[objectName], nil
}

func (r *contractPolicyFanoutRepo) ListEffectiveObjectPermissionsForUser(context.Context, string, string) ([]models.ObjectPermissions, error) {
	r.bulkLoads++
	if r.bulkErr != nil {
		return nil, r.bulkErr
	}
	grants := r.grants
	if r.bulkGrants != nil {
		grants = r.bulkGrants
	}
	perms := make([]models.ObjectPermissions, 0, len(grants))
	for _, grant := range grants {
		perms = append(perms, *grant)
	}
	return perms, nil
}

func TestContractPolicyDescribeGlobalFanoutUsesConstantObjectPermissionLoads(t *testing.T) {
	const objectCount = 400
	grants := make(map[string]*models.ObjectPermissions, objectCount)
	for i := 0; i < objectCount; i++ {
		name := fmt.Sprintf("Object%03d", i)
		grants[name] = &models.ObjectPermissions{SobjectType: name, PermissionsRead: true}
	}

	repo := &contractPolicyFanoutRepo{saveTestRepo: &saveTestRepo{}, grants: grants}
	service := NewPermissionService(repo, &cacheTestMetadata{schema: &models.ObjectMetadata{APIName: "Object000"}}, nil)
	policy := NewContractPolicy(service)
	ctx := WithAccessInputCache(context.Background())
	user := &models.UserSession{ID: "user-1", ProfileID: "profile-1"}

	for i := 0; i < objectCount; i++ {
		name := fmt.Sprintf("Object%03d", i)
		access, err := policy.Access(ctx, user, name, contract.AccessRequest{})
		if err != nil {
			t.Fatalf("Access(%s) error = %v", name, err)
		}
		if access == nil || !access.Object.Read {
			t.Fatalf("Access(%s) = %#v; want readable", name, access)
		}
	}

	if repo.pointLoads > 2 || repo.bulkLoads != 1 {
		t.Fatalf("400-object Describe Global fanout used point=%d bulk=%d repository loads; want at most two point loads and exactly one bulk load", repo.pointLoads, repo.bulkLoads)
	}
}

func TestContractPolicyObjectPermissionBulkFailureFallsBackToPointLoads(t *testing.T) {
	const objectCount = 4
	grants := make(map[string]*models.ObjectPermissions, objectCount)
	for i := 0; i < objectCount; i++ {
		name := fmt.Sprintf("Object%03d", i)
		grants[name] = &models.ObjectPermissions{SobjectType: name, PermissionsRead: true}
	}

	repo := &contractPolicyFanoutRepo{
		saveTestRepo: &saveTestRepo{},
		grants:       grants,
		bulkErr:      errors.New("bulk unavailable"),
	}
	policy := NewContractPolicy(NewPermissionService(repo, &cacheTestMetadata{schema: &models.ObjectMetadata{APIName: "Object000"}}, nil))
	ctx := WithAccessInputCache(context.Background())
	user := &models.UserSession{ID: "user-1", ProfileID: "profile-1"}

	for i := 0; i < objectCount; i++ {
		name := fmt.Sprintf("Object%03d", i)
		access, err := policy.Access(ctx, user, name, contract.AccessRequest{})
		if err != nil || access == nil || !access.Object.Read {
			t.Fatalf("Access(%s) = (%#v, %v); want readable point-query fallback", name, access, err)
		}
	}

	if repo.pointLoads != objectCount || repo.bulkLoads != 1 {
		t.Fatalf("bulk failure used point=%d bulk=%d repository loads; want point=%d bulk=1", repo.pointLoads, repo.bulkLoads, objectCount)
	}
}

func TestContractPolicySuccessfulBulkLoadCachesMissingObjectsAsDeny(t *testing.T) {
	grants := map[string]*models.ObjectPermissions{
		"Object000": {SobjectType: "Object000", PermissionsRead: true},
		"Object001": {SobjectType: "Object001", PermissionsRead: true},
		"Object002": {SobjectType: "Object002", PermissionsRead: true},
		"Object003": {SobjectType: "Object003", PermissionsRead: true},
	}
	repo := &contractPolicyFanoutRepo{
		saveTestRepo: &saveTestRepo{},
		grants:       grants,
		bulkGrants: map[string]*models.ObjectPermissions{
			"Object000": grants["Object000"],
			"Object001": grants["Object001"],
		},
	}
	policy := NewContractPolicy(NewPermissionService(repo, &cacheTestMetadata{schema: &models.ObjectMetadata{APIName: "Object000"}}, nil))
	ctx := WithAccessInputCache(context.Background())
	user := &models.UserSession{ID: "user-1", ProfileID: "profile-1"}

	for _, name := range []string{"Object000", "Object001"} {
		access, err := policy.Access(ctx, user, name, contract.AccessRequest{})
		if err != nil || access == nil || !access.Object.Read {
			t.Fatalf("Access(%s) = (%#v, %v); want readable point result", name, access, err)
		}
	}
	for _, name := range []string{"Object002", "Object003"} {
		access, err := policy.Access(ctx, user, name, contract.AccessRequest{})
		if err != nil || access == nil || access.Object.Read {
			t.Fatalf("Access(%s) = (%#v, %v); want deny from successful bulk projection", name, access, err)
		}
	}

	if repo.pointLoads != 2 || repo.bulkLoads != 1 {
		t.Fatalf("missing bulk rows used point=%d bulk=%d repository loads; want point=2 bulk=1", repo.pointLoads, repo.bulkLoads)
	}
}

func TestContractEffectiveObjectAccessFromModelCopiesCarrier(t *testing.T) {
	source := &models.EffectiveObjectAccess{
		Object: models.ObjectAccess{Create: true, Read: true, Deactivate: true},
		Fields: map[string]models.FieldAccess{
			"Name": {Createable: true, Updateable: true, Readable: true},
		},
		RecordTypes: []models.RecordTypeAvailability{{
			ID:            "012-master",
			DeveloperName: "Master",
			Name:          "Master",
			Available:     true,
			Master:        true,
			Default:       true,
		}},
		DefaultRecordTypeID:    "012-master",
		HasConcreteRecordTypes: true,
		AvailableConcreteCount: 1,
	}

	got := contractEffectiveObjectAccessFromModel(source)
	if got == nil {
		t.Fatal("contractEffectiveObjectAccessFromModel() = nil")
	}
	if !got.Object.Create || !got.Object.Read || got.Object.Update || got.Object.Delete || !got.Object.Deactivate {
		t.Fatalf("Object access = %#v", got.Object)
	}
	if !got.Fields["Name"].Createable || !got.Fields["Name"].Readable {
		t.Fatalf("Fields = %#v", got.Fields)
	}
	if len(got.RecordTypes) != 1 || !got.RecordTypes[0].Master || got.RecordTypes[0].ID != "012-master" {
		t.Fatalf("RecordTypes = %#v", got.RecordTypes)
	}
	if got.DefaultRecordTypeID != "012-master" || !got.HasConcreteRecordTypes || got.AvailableConcreteCount != 1 {
		t.Fatalf("record type summary = %#v", got)
	}

	source.Fields["Name"] = models.FieldAccess{}
	source.RecordTypes[0].ID = "mutated"
	if !got.Fields["Name"].Createable {
		t.Fatalf("Fields aliased model carrier: %#v", got.Fields)
	}
	if got.RecordTypes[0].ID != "012-master" {
		t.Fatalf("RecordTypes aliased model carrier: %#v", got.RecordTypes)
	}
}

func TestContractEffectiveObjectAccessFromModelPreservesNil(t *testing.T) {
	if got := contractEffectiveObjectAccessFromModel(nil); got != nil {
		t.Fatalf("contractEffectiveObjectAccessFromModel(nil) = %#v, want nil", got)
	}
	if got := contractFieldAccessMapFromModel(nil); got != nil {
		t.Fatalf("contractFieldAccessMapFromModel(nil) = %#v, want nil", got)
	}
	if got := contractRecordTypeAvailabilityFromModel(nil); got != nil {
		t.Fatalf("contractRecordTypeAvailabilityFromModel(nil) = %#v, want nil", got)
	}
}
