package dialectsfcrm

import (
	"testing"

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

// The selected Runtime mutation slice does not migrate source-backed
// ApprovalProcess authoring. These L1 policies keep public CRUD from reaching
// the ownerless child materializer, while still allowing ProcessDefinition
// updates that cannot change Metadata to remain an L1 policy decision rather
// than an L0 object-wide admission rule.
func TestRequiredRecordMutationPoliciesKeepApprovalProcessAuthoringOutsideSelectedPublicCRUD(t *testing.T) {
	policies := requiredMutationPoliciesByObject(t)
	definition := policies["ProcessDefinition"]

	for _, surface := range []string{
		models.RecordMutationSurfaceSingleCreate,
		models.RecordMutationSurfaceCollectionCreate,
		models.RecordMutationSurfaceCollectionUpdate,
	} {
		if !hasUnconditionalMutationDenial(definition, surface) {
			t.Fatalf("ProcessDefinition %s is not closed by the required public-mutation policy", surface)
		}
	}
	if !hasFieldPresentMutationDenial(definition, models.RecordMutationSurfaceSingleUpdate, "Metadata") {
		t.Fatal("ProcessDefinition single update does not deny a present Metadata field")
	}
	if hasUnconditionalMutationDenial(definition, models.RecordMutationSurfaceSingleUpdate) {
		t.Fatal("ProcessDefinition single update became an object-wide denial; updates that cannot change Metadata must remain policy-evaluated")
	}

	selectedSurfaces := []string{
		models.RecordMutationSurfaceSingleCreate,
		models.RecordMutationSurfaceSingleUpdate,
		models.RecordMutationSurfaceSingleDelete,
		models.RecordMutationSurfaceCollectionCreate,
		models.RecordMutationSurfaceCollectionUpdate,
		models.RecordMutationSurfaceCollectionDelete,
	}
	for _, objectAPIName := range []string{"ProcessNode", "ProcessAction"} {
		policy, ok := policies[objectAPIName]
		if !ok {
			t.Fatalf("%s required mutation policy is missing", objectAPIName)
		}
		for _, surface := range selectedSurfaces {
			if !hasUnconditionalMutationDenial(policy, surface) {
				t.Fatalf("%s %s is not closed by the required public-mutation policy", objectAPIName, surface)
			}
		}
	}

	lifecyclePolicies := RequiredObjectLifecyclePolicies()
	var processDefinitionPolicy *models.RecordLifecycleAccessPolicy
	for index := range lifecyclePolicies {
		if lifecyclePolicies[index].ObjectAPIName == "ProcessDefinition" {
			processDefinitionPolicy = &lifecyclePolicies[index].Policy
			break
		}
	}
	if processDefinitionPolicy == nil {
		t.Fatalf("ProcessDefinition lifecycle policy = %#v", lifecyclePolicies)
	}
	hooks := processDefinitionPolicy.Hooks
	if !hasLifecycleCapability(hooks, "after_create_in_tx") || !hasLifecycleCapability(hooks, "after_update_in_tx") {
		t.Fatalf("approval lifecycle hooks = %#v", hooks)
	}
}

func TestRequiredRecordMutationPoliciesKeepIdentitySyncPolicyPackOwned(t *testing.T) {
	policies := requiredMutationPoliciesByObject(t)
	for _, objectAPIName := range []string{"IdentitySyncSetting", "IdentitySyncDeactivationTicket"} {
		policy, ok := policies[objectAPIName]
		if !ok {
			t.Fatalf("%s required mutation policy is missing", objectAPIName)
		}

		for _, surface := range []string{
			models.RecordMutationSurfaceSingleCreate,
			models.RecordMutationSurfaceSingleUpdate,
			models.RecordMutationSurfaceSingleDelete,
			models.RecordMutationSurfaceExternalIDUpsert,
			models.RecordMutationSurfaceCollectionCreate,
			models.RecordMutationSurfaceCollectionUpdate,
			models.RecordMutationSurfaceCollectionDelete,
			models.RecordMutationSurfaceCollectionUpsert,
			models.RecordMutationSurfaceTreeCreate,
		} {
			if !hasUnconditionalMutationDenial(policy, surface) {
				t.Fatalf("%s %s is not closed by the required public-mutation policy", objectAPIName, surface)
			}
		}
	}
}

func TestRequiredObjectLifecyclePoliciesHandleDeletedOwnerAndRestore(t *testing.T) {
	byObject := make(map[string]models.RecordLifecycleAccessPolicy)
	for _, policy := range RequiredObjectLifecyclePolicies() {
		if _, duplicate := byObject[policy.ObjectAPIName]; duplicate {
			t.Fatalf("duplicate lifecycle policy for %s", policy.ObjectAPIName)
		}
		byObject[policy.ObjectAPIName] = policy.Policy
	}
	for _, objectAPIName := range []string{"Lead", "Contact"} {
		policy, ok := byObject[objectAPIName]
		if !ok {
			t.Fatalf("%s deleted-owner lifecycle policy is missing", objectAPIName)
		}
		for _, phase := range []string{"after_delete_in_tx", "after_update_in_tx"} {
			if !hasNamedLifecycleCapability(policy.Hooks, phase, "connector.remote-identity.owner-delete") {
				t.Fatalf("%s lifecycle hooks = %#v, want connector remote identity owner lifecycle hook at %s", objectAPIName, policy.Hooks, phase)
			}
		}
	}
}

func requiredMutationPoliciesByObject(t *testing.T) map[string]models.RecordMutationAccessPolicy {
	t.Helper()
	policies := RequiredRecordMutationPolicies()
	byObject := make(map[string]models.RecordMutationAccessPolicy, len(policies))
	for _, policy := range policies {
		if policy.ObjectAPIName == "" {
			t.Fatal("required mutation policy has an empty object API name")
		}
		if _, duplicate := byObject[policy.ObjectAPIName]; duplicate {
			t.Fatalf("duplicate required mutation policy for %s", policy.ObjectAPIName)
		}
		byObject[policy.ObjectAPIName] = policy.Policy
	}
	return byObject
}

func hasUnconditionalMutationDenial(policy models.RecordMutationAccessPolicy, surface string) bool {
	for _, denial := range policy.Deny {
		if denial.When != nil || !hasMutationSurface(denial.Surfaces, surface) {
			continue
		}
		return true
	}
	return false
}

func hasFieldPresentMutationDenial(policy models.RecordMutationAccessPolicy, surface, field string) bool {
	for _, denial := range policy.Deny {
		if !hasMutationSurface(denial.Surfaces, surface) || denial.When == nil || denial.When.FieldPresent == nil {
			continue
		}
		if denial.Field == field && denial.When.FieldPresent.Field == field {
			return true
		}
	}
	return false
}

func hasMutationSurface(surfaces []string, want string) bool {
	for _, surface := range surfaces {
		if surface == want {
			return true
		}
	}
	return false
}

func hasLifecycleCapability(hooks []models.RecordLifecycleHookPolicy, phase string) bool {
	return hasNamedLifecycleCapability(hooks, phase, ApprovalProcessChildSyncCapability)
}

func hasNamedLifecycleCapability(hooks []models.RecordLifecycleHookPolicy, phase, capability string) bool {
	for _, hook := range hooks {
		if hook.Phase == phase && hook.Capability == capability {
			return true
		}
	}
	return false
}
