package permissions

import (
	"context"
	"testing"

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

type accessPolicyMetadata struct {
	schemas map[string]*models.ObjectMetadata
}

func (m *accessPolicyMetadata) GetSchema(ctx context.Context, apiName string) *models.ObjectMetadata {
	return m.schemas[apiName]
}

func (m *accessPolicyMetadata) GetSchemas(ctx context.Context) []*models.ObjectMetadata {
	out := make([]*models.ObjectMetadata, 0, len(m.schemas))
	for _, schema := range m.schemas {
		out = append(out, schema)
	}
	return out
}

func (m *accessPolicyMetadata) GetRelationships(ctx context.Context, objectAPIName string) []*models.Relationship {
	return nil
}

func (m *accessPolicyMetadata) GetSharingParentRelationship(ctx context.Context, childObjectAPIName string) *models.Relationship {
	return nil
}

func (m *accessPolicyMetadata) GetSharingRules(ctx context.Context, objectAPIName string) []*models.SharingRule {
	return nil
}

func TestEvaluateObjectAccessPolicy_CompositionOperators(t *testing.T) {
	user := &models.UserSession{ID: "u1", ProfileID: "p1"}
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"Account": {APIName: "Account"},
		"Derived__c": {
			APIName: "Derived__c",
			AccessPolicy: &models.ObjectAccessPolicy{
				Mode: "derived",
				ObjectRead: &models.AccessPolicyExpr{
					AnyOf: []models.AccessPolicyExpr{
						{
							AllOf: []models.AccessPolicyExpr{
								{Kind: "objectPermission", Object: "Account", Operation: constants.PermRead},
								{Not: &models.AccessPolicyExpr{Kind: "systemPermission", APIName: "BlockedByPolicy"}},
							},
						},
						{Kind: "systemPermission", APIName: "FallbackRead"},
					},
				},
				FieldRead: &models.AccessPolicyExpr{Inherit: "objectRead"},
			},
		},
	}}
	repo := &mockPermRepo{
		loadEffectiveObjectPerm: func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
			if objectAPIName == "Account" {
				return &models.ObjectPermissions{SobjectType: "Account", PermissionsRead: true}, nil
			}
			return nil, nil
		},
		getEffSysPerms: func(ctx context.Context, userID, profileID string) (map[string]bool, error) {
			return map[string]bool{
				"BlockedByPolicy": false,
				"FallbackRead":    false,
			}, nil
		},
	}
	ps := NewPermissionService(repo, meta, nil)

	allowed, err := ps.EvaluateObjectAccessPolicy(context.Background(), meta.schemas["Derived__c"], constants.PermRead, user)
	if err != nil {
		t.Fatalf("EvaluateObjectAccessPolicy() error = %v", err)
	}
	if !allowed {
		t.Fatal("expected derived policy read to be allowed")
	}
	if !ps.CheckFieldVisibilityWithUser(context.Background(), "Derived__c", "Name", user) {
		t.Fatal("expected field read to inherit object read")
	}
}

func TestCheckObjectPermissionWithUser_AccessPolicyUsesReferencedObjectPermissions(t *testing.T) {
	user := &models.UserSession{ID: "u1", ProfileID: "p1"}
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"Opportunity": {APIName: "Opportunity"},
		"Contact":     {APIName: "Contact"},
		"OpportunityContactRole": {
			APIName: "OpportunityContactRole",
			AccessPolicy: &models.ObjectAccessPolicy{
				Mode: "derived",
				ObjectRead: &models.AccessPolicyExpr{
					AllOf: []models.AccessPolicyExpr{
						{Kind: "objectPermission", Object: "Opportunity", Operation: constants.PermRead},
						{Kind: "objectPermission", Object: "Contact", Operation: constants.PermRead},
					},
				},
				ObjectCreate: &models.AccessPolicyExpr{
					AllOf: []models.AccessPolicyExpr{
						{Kind: "objectPermission", Object: "Opportunity", Operation: constants.PermEdit},
						{Kind: "objectPermission", Object: "Contact", Operation: constants.PermRead},
					},
				},
				ObjectEdit: &models.AccessPolicyExpr{
					AllOf: []models.AccessPolicyExpr{
						{Kind: "objectPermission", Object: "Opportunity", Operation: constants.PermEdit},
						{Kind: "objectPermission", Object: "Contact", Operation: constants.PermRead},
					},
				},
				ObjectDelete: &models.AccessPolicyExpr{
					AllOf: []models.AccessPolicyExpr{
						{Kind: "objectPermission", Object: "Opportunity", Operation: constants.PermEdit},
						{Kind: "objectPermission", Object: "Contact", Operation: constants.PermRead},
					},
				},
			},
		},
	}}
	repo := &mockPermRepo{
		loadEffectiveObjectPerm: func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
			switch objectAPIName {
			case "Opportunity":
				return &models.ObjectPermissions{SobjectType: "Opportunity", PermissionsRead: true, PermissionsEdit: true}, nil
			case "Contact":
				return &models.ObjectPermissions{SobjectType: "Contact", PermissionsRead: false}, nil
			default:
				return nil, nil
			}
		},
	}
	ps := NewPermissionService(repo, meta, nil)

	if ps.CheckObjectPermissionWithUser(context.Background(), "OpportunityContactRole", constants.PermRead, user) {
		t.Fatal("expected OCR read to fail without Contact.Read")
	}

	repo.loadEffectiveObjectPerm = func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
		switch objectAPIName {
		case "Opportunity":
			return &models.ObjectPermissions{SobjectType: "Opportunity", PermissionsRead: true, PermissionsEdit: true}, nil
		case "Contact":
			return &models.ObjectPermissions{SobjectType: "Contact", PermissionsRead: true}, nil
		default:
			return nil, nil
		}
	}

	if !ps.CheckObjectPermissionWithUser(context.Background(), "OpportunityContactRole", constants.PermRead, user) {
		t.Fatal("expected OCR read to succeed with Opportunity.Read + Contact.Read")
	}
	if !ps.CheckObjectPermissionWithUser(context.Background(), "OpportunityContactRole", constants.PermCreate, user) {
		t.Fatal("expected OCR create to succeed with Opportunity.Edit + Contact.Read")
	}
	if !ps.CheckObjectPermissionWithUser(context.Background(), "OpportunityContactRole", constants.PermDelete, user) {
		t.Fatal("expected OCR delete to succeed with Opportunity.Edit + Contact.Read")
	}
}

func TestCheckFieldEditabilityWithUser_DerivedPolicyUsesExplicitFieldPolicy(t *testing.T) {
	user := &models.UserSession{ID: "u1", ProfileID: "p1"}
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"Derived__c": {
			APIName: "Derived__c",
			AccessPolicy: &models.ObjectAccessPolicy{
				Mode:       "derived",
				ObjectEdit: &models.AccessPolicyExpr{Kind: "systemPermission", APIName: "ObjectEditDenied"},
				FieldEdit:  &models.AccessPolicyExpr{Kind: "systemPermission", APIName: "FieldEditAllowed"},
			},
		},
	}}
	repo := &mockPermRepo{
		getEffSysPerms: func(ctx context.Context, userID, profileID string) (map[string]bool, error) {
			return map[string]bool{
				"ObjectEditDenied": false,
				"FieldEditAllowed": true,
			}, nil
		},
	}
	ps := NewPermissionService(repo, meta, nil)

	if !ps.CheckFieldEditabilityWithUser(context.Background(), "Derived__c", "Name", user) {
		t.Fatal("expected explicit field edit policy to be honored")
	}
}

func TestFieldAccessResolver_CheckFieldEditWithCreateFallback_DerivedPolicySkipsSelfFieldPermissions(t *testing.T) {
	user := &models.UserSession{ID: "u1", ProfileID: "p1"}
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"Opportunity": {APIName: "Opportunity"},
		"Contact":     {APIName: "Contact"},
		"OpportunityContactRole": {
			APIName: "OpportunityContactRole",
			AccessPolicy: &models.ObjectAccessPolicy{
				Mode: "derived",
				ObjectCreate: &models.AccessPolicyExpr{
					AllOf: []models.AccessPolicyExpr{
						{Kind: "objectPermission", Object: "Opportunity", Operation: constants.PermEdit},
						{Kind: "objectPermission", Object: "Contact", Operation: constants.PermRead},
					},
				},
			},
		},
	}}
	var fieldLookups []string
	repo := &mockPermRepo{
		loadEffectiveObjectPerm: func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
			switch objectAPIName {
			case "Opportunity":
				return &models.ObjectPermissions{SobjectType: "Opportunity", PermissionsEdit: true}, nil
			case "Contact":
				return &models.ObjectPermissions{SobjectType: "Contact", PermissionsRead: true}, nil
			default:
				return nil, nil
			}
		},
		loadEffectiveFieldPerm: func(ctx context.Context, user *models.UserSession, objectAPIName, fieldAPIName string) (*models.FieldPermissions, error) {
			fieldLookups = append(fieldLookups, objectAPIName+"."+fieldAPIName)
			return &models.FieldPermissions{PermissionsEdit: false}, nil
		},
	}
	ps := NewPermissionService(repo, meta, nil)
	resolver := NewFieldAccessResolver(ps)

	if !resolver.checkFieldEditWithCreateFallback(context.Background(), "OpportunityContactRole", "OpportunityId", user) {
		t.Fatal("expected derived create-field gate to ignore stale self field permissions once objectCreate passed")
	}
	if len(fieldLookups) != 0 {
		t.Fatalf("derived create-field gate should not read self field permissions, got %v", fieldLookups)
	}
}

func TestCheckObjectPermissionWithUser_NonDerivedPolicyFallsBackToSelfPermissions(t *testing.T) {
	user := &models.UserSession{ID: "u1", ProfileID: "p1"}
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"FutureMode__c": {
			APIName: "FutureMode__c",
			AccessPolicy: &models.ObjectAccessPolicy{
				Mode:       "future",
				ObjectRead: &models.AccessPolicyExpr{Kind: "systemPermission", APIName: "DeniedByPolicy"},
			},
		},
	}}
	repo := &mockPermRepo{
		loadEffectiveObjectPerm: func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
			if objectAPIName == "FutureMode__c" {
				return &models.ObjectPermissions{SobjectType: "FutureMode__c", PermissionsRead: true}, nil
			}
			return nil, nil
		},
		getEffSysPerms: func(ctx context.Context, userID, profileID string) (map[string]bool, error) {
			return map[string]bool{"DeniedByPolicy": false}, nil
		},
	}
	ps := NewPermissionService(repo, meta, nil)

	if !ps.CheckObjectPermissionWithUser(context.Background(), "FutureMode__c", constants.PermRead, user) {
		t.Fatal("expected non-derived access policy objects to keep self object-permission ownership")
	}
}

func TestBuildPermissionsForProfileAndSchema_DerivedPolicySkipsSelfRows(t *testing.T) {
	objPerms, fieldPerms := BuildPermissionsForProfileAndSchema(constants.ProfileStandardUser, "ps1", &models.ObjectMetadata{
		APIName: "OpportunityContactRole",
		Fields: []models.FieldMetadata{
			{APIName: "OpportunityId"},
			{APIName: "ContactId"},
		},
		AccessPolicy: &models.ObjectAccessPolicy{Mode: "derived"},
	})

	if len(objPerms) != 0 || len(fieldPerms) != 0 {
		t.Fatalf("derived access policy should skip self permission rows, got obj=%d field=%d", len(objPerms), len(fieldPerms))
	}
}

func TestBuildPermissionsForProfileAndSchema_NonDerivedPolicyKeepsSelfRows(t *testing.T) {
	objPerms, fieldPerms := BuildPermissionsForProfileAndSchema(constants.ProfileStandardUser, "ps1", &models.ObjectMetadata{
		APIName: "FutureMode__c",
		Fields: []models.FieldMetadata{
			{APIName: "Name"},
		},
		AccessPolicy: &models.ObjectAccessPolicy{Mode: "future"},
	})

	if len(objPerms) != 1 || len(fieldPerms) != 1 {
		t.Fatalf("non-derived access policy should keep self permission rows, got obj=%d field=%d", len(objPerms), len(fieldPerms))
	}
}

func TestSeedFieldPermissionsForFields_DerivedPolicySkipsSelfRows(t *testing.T) {
	ctx := context.Background()
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"CaseComment": {
			APIName: "CaseComment",
			Fields: []models.FieldMetadata{
				{APIName: "CommentBody"},
				{APIName: "IsPublished"},
			},
			AccessPolicy: &models.ObjectAccessPolicy{Mode: "derived"},
		},
	}}

	batchCalled := false
	repo := &mockPermRepo{
		getAllProfiles: func(ctx context.Context) ([]*models.Profile, error) {
			return []*models.Profile{
				{ID: constants.ProfileSystemAdmin, Name: "System Administrator"},
				{ID: constants.ProfileStandardUser, Name: "Standard User"},
			}, nil
		},
		batchUpsertFieldPerms: func(ctx context.Context, perms []models.FieldPermissions) error {
			batchCalled = true
			if len(perms) != 0 {
				t.Fatalf("expected no field permissions for derived policy objects, got %d", len(perms))
			}
			return nil
		},
	}
	ps := NewPermissionService(repo, meta, nil)

	if err := ps.SeedFieldPermissionsForFields(ctx, "CaseComment", []string{"CommentBody"}); err != nil {
		t.Fatalf("SeedFieldPermissionsForFields() error = %v", err)
	}
	if batchCalled {
		t.Fatal("expected derived policy field seeding to skip BatchUpsertFieldPermissions entirely")
	}
}

func TestCheckObjectPermissionOnRecord_CaseCommentDelegatedPolicyUsesActorSensitiveTerms(t *testing.T) {
	user := &models.UserSession{ID: "u1", ProfileID: "p1"}
	schema := &models.ObjectMetadata{
		APIName: "CaseComment",
		AccessPolicy: &models.ObjectAccessPolicy{
			Mode: "derived",
			ObjectRead: &models.AccessPolicyExpr{
				Kind:      "objectPermission",
				Object:    "Case",
				Operation: constants.PermRead,
			},
			ObjectEdit: &models.AccessPolicyExpr{
				AllOf: []models.AccessPolicyExpr{
					{Kind: "objectPermission", Object: "Case", Operation: constants.PermEdit},
					{
						AnyOf: []models.AccessPolicyExpr{
							{Kind: "objectPermission", Object: "Case", Operation: "ModifyAllRecords"},
							{
								AllOf: []models.AccessPolicyExpr{
									{Kind: "systemPermission", APIName: "EditCaseComments"},
									{Kind: "recordFieldEqualsCurrentUser", Field: "CreatedById"},
								},
							},
						},
					},
				},
			},
			FieldEdit: &models.AccessPolicyExpr{Inherit: "objectEdit"},
		},
	}
	meta := &accessPolicyMetadata{schemas: map[string]*models.ObjectMetadata{
		"Case":        {APIName: "Case"},
		"CaseComment": schema,
	}}
	repo := &mockPermRepo{
		loadEffectiveObjectPerm: func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
			if objectAPIName != "Case" {
				return nil, nil
			}
			return &models.ObjectPermissions{SobjectType: "Case", PermissionsRead: true}, nil
		},
		getEffSysPerms: func(ctx context.Context, userID, profileID string) (map[string]bool, error) {
			return map[string]bool{"EditCaseComments": true}, nil
		},
	}
	ps := NewPermissionService(repo, meta, nil)

	if ps.CheckObjectPermissionWithUser(context.Background(), "CaseComment", constants.PermEdit, user) {
		t.Fatal("expected record-sensitive edit policy to fail without record context")
	}

	ownComment := models.SObject{
		constants.FieldID:          "cc1",
		constants.FieldCreatedByID: user.ID,
	}
	if ps.CheckObjectPermissionOnRecord(context.Background(), schema, ownComment, constants.PermEdit, user) {
		t.Fatal("expected own-comment edit to fail without Case.Edit baseline")
	}

	repo.loadEffectiveObjectPerm = func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
		if objectAPIName != "Case" {
			return nil, nil
		}
		return &models.ObjectPermissions{SobjectType: "Case", PermissionsRead: true, PermissionsEdit: true}, nil
	}
	if !ps.CheckObjectPermissionOnRecord(context.Background(), schema, ownComment, constants.PermEdit, user) {
		t.Fatal("expected own-comment edit to pass with Case.Edit + EditCaseComments")
	}
	if !ps.CheckFieldEditabilityOnRecord(context.Background(), "CaseComment", "CommentBody", ownComment, user) {
		t.Fatal("expected field edit to inherit delegated object edit on own comment")
	}

	otherComment := models.SObject{
		constants.FieldID:          "cc2",
		constants.FieldCreatedByID: "u2",
	}
	if ps.CheckObjectPermissionOnRecord(context.Background(), schema, otherComment, constants.PermEdit, user) {
		t.Fatal("expected another user's comment edit to fail without Case.ModifyAllRecords")
	}

	repo.loadEffectiveObjectPerm = func(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
		if objectAPIName != "Case" {
			return nil, nil
		}
		return &models.ObjectPermissions{SobjectType: "Case", PermissionsRead: true, PermissionsEdit: true, PermissionsModifyAllRecords: true}, nil
	}
	if !ps.CheckObjectPermissionOnRecord(context.Background(), schema, otherComment, constants.PermEdit, user) {
		t.Fatal("expected Case.Edit + Case.ModifyAllRecords to override actor-sensitive edit branch")
	}
}
