package permissions

import (
	"context"
	"errors"
	"testing"

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

type aggregateParentAccessRepo struct {
	pcRepo
	pendingLockCalls int
	manualBatchCalls int
	teamBatchCalls   int
	manualPointCalls int
	teamPointCalls   int
	manualPointErr   error
	teamPointErr     error
}

func (r *aggregateParentAccessRepo) GetPendingApprovalLockInfo(context.Context, string) (string, string, error) {
	r.pendingLockCalls++
	return "", "", nil
}

func (r *aggregateParentAccessRepo) GetManualShareAccessLevelsByRecordIDs(
	_ context.Context,
	_ string,
	_ string,
	_ []string,
) (map[string][]string, error) {
	r.manualBatchCalls++
	return map[string][]string{}, nil
}

func (r *aggregateParentAccessRepo) GetTeamMemberAccessLevelsByRecordIDs(
	_ context.Context,
	_ string,
	_ string,
	_ []string,
) (map[string]string, error) {
	r.teamBatchCalls++
	return map[string]string{}, nil
}

func (r *aggregateParentAccessRepo) GetManualShareAccessLevels(context.Context, string, string, string) ([]string, error) {
	r.manualPointCalls++
	if r.manualPointErr != nil {
		return nil, r.manualPointErr
	}
	return []string{"Nope"}, nil
}

func (r *aggregateParentAccessRepo) GetTeamMemberAccessLevel(context.Context, string, string, string) (*string, error) {
	r.teamPointCalls++
	if r.teamPointErr != nil {
		return nil, r.teamPointErr
	}
	level := "Full"
	return &level, nil
}

func newAggregateParentAccessFixture() (*PermissionService, *aggregateParentAccessRepo, *models.ObjectMetadata, models.SObject, *models.UserSession) {
	child := &models.ObjectMetadata{
		APIName:      "Contact",
		SharingModel: constants.SharingModelControlledByParent,
		Fields: []models.FieldMetadata{{
			APIName:  "AccountId",
			Type:     constants.FieldTypeReference,
			Required: true,
		}},
	}
	parent := &models.ObjectMetadata{
		APIName:      "Account",
		SharingModel: constants.SharingModelPrivate,
	}
	metadata := &pcMetadataStub{
		schemas: map[string]*models.ObjectMetadata{
			child.APIName:  child,
			parent.APIName: parent,
		},
		sharingParent: map[string]*models.Relationship{
			child.APIName: {
				ChildObjectAPIName:  child.APIName,
				ParentObjectAPIName: parent.APIName,
				FieldAPIName:        "AccountId",
				IsSharingParent:     true,
			},
		},
	}
	user := &models.UserSession{ID: "user-1", ProfileID: "profile-1"}
	repo := &aggregateParentAccessRepo{pcRepo: pcRepo{
		objPerms: map[string]*models.ObjectPermissions{
			parent.APIName: {
				SobjectType:       parent.APIName,
				PermissionsRead:   true,
				PermissionsEdit:   true,
				PermissionsDelete: true,
			},
		},
		systemPerms: map[string]bool{},
		records: map[string]models.SObject{
			"Account:account-1": {
				constants.FieldID:      "account-1",
				constants.FieldOwnerID: "parent-owner",
			},
		},
	}}
	service := NewPermissionService(repo, metadata, nil)
	record := models.SObject{
		constants.FieldID:      "contact-1",
		constants.FieldOwnerID: "another-owner",
		"AccountId":            "account-1",
	}
	return service, repo, child, record, user
}

func TestCheckRecordAccessAllReusesParentControlledAuthoritySnapshot(t *testing.T) {
	service, repo, schema, record, user := newAggregateParentAccessFixture()
	ctx := context.Background()

	access := service.CheckRecordAccessAll(ctx, schema, record, user)
	if !access.HasReadAccess || !access.HasEditAccess || !access.HasDeleteAccess || access.IsLocked {
		t.Fatalf("aggregate access = %#v, want read/edit/delete and unlocked", access)
	}
	if repo.getByIDCalls != 1 {
		t.Fatalf("parent record loads = %d, want 1 shared by Read/Edit/Delete", repo.getByIDCalls)
	}
	if repo.manualPointCalls != 1 || repo.teamPointCalls != 1 {
		t.Fatalf("parent grant point loads manual/team = %d/%d, want one successful snapshot each", repo.manualPointCalls, repo.teamPointCalls)
	}
	if repo.manualBatchCalls != 0 || repo.teamBatchCalls != 0 {
		t.Fatalf("single parent decision used eager batch loads manual/team = %d/%d, want 0/0", repo.manualBatchCalls, repo.teamBatchCalls)
	}
	if repo.pendingLockCalls != 2 {
		t.Fatalf("approval lock loads = %d, want child once plus parent once", repo.pendingLockCalls)
	}
}

func TestCheckRecordAccessAllDoesNotReuseParentSnapshotAcrossDecisions(t *testing.T) {
	service, repo, schema, record, user := newAggregateParentAccessFixture()
	ctx := WithAccessInputCache(context.Background())

	for i := 0; i < 2; i++ {
		access := service.CheckRecordAccessAll(ctx, schema, record, user)
		if !access.HasReadAccess || !access.HasEditAccess || !access.HasDeleteAccess {
			t.Fatalf("decision %d aggregate access = %#v, want read/edit/delete", i+1, access)
		}
	}
	if repo.getByIDCalls != 2 {
		t.Fatalf("parent record loads = %d, want one fresh load per aggregate decision", repo.getByIDCalls)
	}
	if repo.pendingLockCalls != 4 {
		t.Fatalf("approval lock loads = %d, want fresh child+parent locks per decision", repo.pendingLockCalls)
	}
	if repo.manualPointCalls != 2 || repo.teamPointCalls != 2 {
		t.Fatalf("parent grant points manual/team = %d/%d, want one fresh snapshot per decision", repo.manualPointCalls, repo.teamPointCalls)
	}
}

func TestBatchCheckRecordAccessSharesOneParentSnapshotAcrossChildren(t *testing.T) {
	service, repo, schema, record, user := newAggregateParentAccessFixture()
	second := cloneRecordAccessSObject(record)
	second[constants.FieldID] = "contact-2"
	ctx := context.Background()

	results := service.BatchCheckRecordAccess(ctx, schema, []models.SObject{record, second}, user)
	if len(results) != 2 || !results["contact-1"].HasReadAccess || !results["contact-2"].HasEditAccess {
		t.Fatalf("batch access = %#v, want both parent-authorized children", results)
	}
	if repo.getByIDCalls != 1 {
		t.Fatalf("batch parent record loads = %d, want 1 for a shared parent", repo.getByIDCalls)
	}
	if repo.manualBatchCalls != 1 || repo.teamBatchCalls != 1 {
		t.Fatalf("child grant batches manual/team = %d/%d, want 1/1", repo.manualBatchCalls, repo.teamBatchCalls)
	}
	if repo.manualPointCalls != 1 || repo.teamPointCalls != 1 {
		t.Fatalf("shared-parent grant points manual/team = %d/%d, want 1/1", repo.manualPointCalls, repo.teamPointCalls)
	}
}

func TestAggregateAccessDoesNotCacheGrantLookupErrors(t *testing.T) {
	service, repo, _, _, user := newAggregateParentAccessFixture()
	ctx := withRecordAccessEvaluationCache(context.Background())
	repo.manualPointErr = errors.New("temporary manual share read failure")
	repo.teamPointErr = errors.New("temporary team member read failure")

	for i := 0; i < 2; i++ {
		if service.checkManualShareAccess(ctx, "Account", "account-1", user, constants.PermRead) {
			t.Fatalf("manual share attempt %d unexpectedly granted", i+1)
		}
		if service.checkTeamMemberAccess(ctx, "Account", "account-1", user, constants.PermRead) {
			t.Fatalf("team member attempt %d unexpectedly granted", i+1)
		}
	}
	if repo.manualPointCalls != 2 || repo.teamPointCalls != 2 {
		t.Fatalf("errored grant loads manual/team = %d/%d, want 2/2 without memoization", repo.manualPointCalls, repo.teamPointCalls)
	}
}
