package dialectsfcrm

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"reflect"
	"strings"
	"testing"

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

func TestPlanApprovalProcessPersistenceProjectsCanonicalRows(t *testing.T) {
	raw := `<ApprovalProcess>
  <label>Deal Approval</label><active>true</active><description>large deals</description>
  <allowRecall>false</allowRecall><recordEditability>AdminOnly</recordEditability>
  <finalApprovalRecordLock>true</finalApprovalRecordLock>
  <entryCriteria><booleanFilter>1 AND 2</booleanFilter>
    <criteriaItems><field> Amount </field><operation>greaterThan</operation><value>1000</value></criteriaItems>
    <criteriaItems><field>Region</field><operation>equals</operation><value>West</value></criteriaItems>
  </entryCriteria><processOrder>7</processOrder>
  <approvalStep><name>Manager</name><label>Manager review</label><description>first gate</description>
    <allowComments>true</allowComments><requireComments>true</requireComments>
    <rejectBehavior><type>BackToPrevious</type></rejectBehavior>
    <assignedApprover><approver><type>User</type><name>one@example.com</name></approver><approver><type>User</type><name>two@example.com</name></approver></assignedApprover>
    <stepApprovalActions><actions><name>Clear Note</name><type>FieldUpdate</type><fieldApiName>Note</fieldApiName><fieldValue>Literal: </fieldValue></actions></stepApprovalActions>
  </approvalStep>
  <initialSubmissionActions><actions><name>Notify Flow</name><type>Flow</type><apiName>Approval_Notify</apiName></actions></initialSubmissionActions>
</ApprovalProcess>`
	metadata, err := DecodeApprovalProcessMetadata(raw)
	if err != nil {
		t.Fatalf("DecodeApprovalProcessMetadata returned error: %v", err)
	}
	plan, err := PlanApprovalProcessPersistence(ApprovalProcessProjectionRequest{
		ObjectAPIName: " Opportunity ", DeveloperName: " Deal_Approval ", ParentDisposition: ApprovalProcessParentManagedUpsert,
		OwnerPack: "crm-standard", RawMetadata: raw, Metadata: metadata,
	})
	if err != nil {
		t.Fatalf("PlanApprovalProcessPersistence returned error: %v", err)
	}

	parent := plan.Parent
	if parent.BusinessKey != (ApprovalProcessBusinessKey{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval"}) || parent.OwnerPack != "crm-standard" {
		t.Fatalf("parent identity = (%#v,%q)", parent.BusinessKey, parent.OwnerPack)
	}
	if parent.Name != "Deal Approval" || parent.Description != "large deals" || parent.State != "Active" || !parent.IsActive || parent.AllowRecall {
		t.Fatalf("parent scalar fields = %#v", parent)
	}
	if parent.EntryFilter != `{"booleanFilter":"1 AND 2","criteriaItems":[{"field":"Amount","operation":"greaterThan","value":"1000"},{"field":"Region","operation":"equals","value":"West"}]}` {
		t.Fatalf("EntryFilter = %q", parent.EntryFilter)
	}
	if parent.LockType != "AdminOnly" || !parent.FinalApprovalRecordLock || parent.FinalRejectionRecordLock || parent.ProcessOrder == nil || *parent.ProcessOrder != 7 {
		t.Fatalf("lock/order fields = %#v", parent)
	}
	wantHash := sha256.Sum256([]byte(raw))
	if parent.RawMetadata != raw || parent.MetadataSHA256 != hex.EncodeToString(wantHash[:]) {
		t.Fatalf("raw metadata identity = (%t,%q)", parent.RawMetadata == raw, parent.MetadataSHA256)
	}
	if !plan.SoftDeleteMissingNodes || !plan.ReplaceProcessActionRows {
		t.Fatalf("reconcile intent = softDelete:%v replaceActions:%v", plan.SoftDeleteMissingNodes, plan.ReplaceProcessActionRows)
	}
	if plan.ParentDisposition != ApprovalProcessParentManagedUpsert || plan.PreAppliedParentID != "" {
		t.Fatalf("parent target = (%q,%q), want managed", plan.ParentDisposition, plan.PreAppliedParentID)
	}

	if len(plan.Nodes) != 1 {
		t.Fatalf("node count = %d, want 1", len(plan.Nodes))
	}
	node := plan.Nodes[0]
	wantNodeKey := ApprovalProcessNodeBusinessKey{Process: parent.BusinessKey, DeveloperName: "Manager"}
	if node.BusinessKey != wantNodeKey || node.Name != "Manager review" || node.StepOrder != 1 {
		t.Fatalf("stable node identity = %#v", node)
	}
	if !node.AllowComments || !node.RequireComments || node.RejectBehavior != constants.ApprovalRejectBehaviorBackToSubmitter {
		t.Fatalf("node policy = %#v", node)
	}
	if node.ApproverType != constants.ApprovalApproverTypeUser || !reflect.DeepEqual(node.ApproverReferences, []string{"one@example.com", "two@example.com"}) {
		t.Fatalf("node approver = %#v", node)
	}

	if len(plan.Actions) != 2 {
		t.Fatalf("action count = %d, want 2", len(plan.Actions))
	}
	actions := map[string]ApprovalProcessActionRowIntent{}
	for _, action := range plan.Actions {
		actions[action.Name] = action
	}
	clear := actions["Clear Note"]
	if clear.ProcessNode == nil || *clear.ProcessNode != wantNodeKey || clear.ActionEvent != constants.ApprovalActionEventStepApproval {
		t.Fatalf("step action binding = %#v", clear)
	}
	if clear.FieldValue == nil || *clear.FieldValue != "" {
		t.Fatalf("explicit empty FieldValue = %#v, want pointer to empty string", clear.FieldValue)
	}
	flow := actions["Notify Flow"]
	if flow.ProcessNode != nil || flow.FieldValue != nil || flow.ActionType != constants.ProcessActionTypeFlow || flow.APIName != "Approval_Notify" {
		t.Fatalf("process action = %#v", flow)
	}
}

func TestPlanApprovalProcessPersistenceOwnsChildDefaultsAndValidation(t *testing.T) {
	base := ApprovalProcessProjectionRequest{
		ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval",
		ParentDisposition: ApprovalProcessParentManagedUpsert, OwnerPack: "crm-standard", RawMetadata: "raw",
	}

	t.Run("node defaults", func(t *testing.T) {
		request := base
		request.Metadata = &ApprovalProcessMetadata{Steps: []ApprovalProcessStep{{Name: "Manager"}}}
		plan, err := PlanApprovalProcessPersistence(request)
		if err != nil {
			t.Fatal(err)
		}
		if len(plan.Nodes) != 1 || !plan.Nodes[0].AllowComments || plan.Nodes[0].RequireComments || plan.Nodes[0].RejectBehavior != constants.ApprovalRejectBehaviorFinalRejection {
			t.Fatalf("default node = %#v", plan.Nodes)
		}
	})

	for _, test := range []struct {
		name, actionType, apiName, wantField string
	}{
		{name: "retired alert", actionType: constants.ProcessActionTypeAlert, wantField: "ActionType"},
		{name: "flow without api name", actionType: constants.ProcessActionTypeFlow, wantField: constants.FieldProcessAction_APIName},
		{name: "unknown action type", actionType: "CustomAction", apiName: "Custom_Action", wantField: constants.FieldProcessAction_ActionType},
	} {
		t.Run(test.name, func(t *testing.T) {
			request := base
			request.Metadata = &ApprovalProcessMetadata{InitialSubmissionActions: []ApprovalProcessAction{{Type: test.actionType, APIName: test.apiName}}}
			_, err := PlanApprovalProcessPersistence(request)
			var validationErr *ValidationError
			if !errors.As(err, &validationErr) || validationErr.ValidationField() != test.wantField {
				t.Fatalf("PlanApprovalProcessPersistence error = %v, want validation field %q", err, test.wantField)
			}
		})
	}
}

func TestPlanApprovalProcessPersistenceRejectsMixedActionBodies(t *testing.T) {
	_, err := PlanApprovalProcessPersistence(ApprovalProcessProjectionRequest{
		ObjectAPIName: "Account", DeveloperName: "Account_Approval",
		ParentDisposition: ApprovalProcessParentManagedUpsert, OwnerPack: "crm-standard",
		RawMetadata: "raw", Metadata: &ApprovalProcessMetadata{
			InitialSubmissionActions: []ApprovalProcessAction{{
				Name: "Notify", Type: constants.ProcessActionTypeFlow,
				EmailTemplateID: "Approval_Submitted", APIName: "Approval_Notification",
			}},
		},
	})
	var validationErr *ValidationError
	if !errors.As(err, &validationErr) || validationErr.ValidationField() != constants.FieldProcessAction_EmailTemplateID {
		t.Fatalf("PlanApprovalProcessPersistence error = %v, want validation field %q", err, constants.FieldProcessAction_EmailTemplateID)
	}
}

func TestPlanApprovalProcessPersistenceRejectsUnknownActionBodyFields(t *testing.T) {
	raw := `<ApprovalProcess><initialSubmissionActions><actions><name>Notify</name><type>Flow</type><apiName>Approval_Notification</apiName><unsupported>value</unsupported></actions></initialSubmissionActions></ApprovalProcess>`
	metadata, err := DecodeApprovalProcessMetadata(raw)
	if err != nil {
		t.Fatal(err)
	}
	_, err = PlanApprovalProcessPersistence(ApprovalProcessProjectionRequest{
		ObjectAPIName: "Account", DeveloperName: "Account_Approval",
		ParentDisposition: ApprovalProcessParentManagedUpsert, OwnerPack: "crm-standard",
		RawMetadata: raw, Metadata: metadata,
	})
	var validationErr *ValidationError
	if !errors.As(err, &validationErr) || validationErr.ValidationField() != constants.FieldProcessAction_ActionType {
		t.Fatalf("PlanApprovalProcessPersistence error = %v, want validation field %q", err, constants.FieldProcessAction_ActionType)
	}
}

func TestProjectApprovalProcessNodeUsesTypedSafeApproverColumns(t *testing.T) {
	process := ApprovalProcessBusinessKey{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval"}
	tests := []struct {
		name, approverType, reference, wantType, wantRelated string
		wantReferences                                       []string
	}{
		{name: "user", approverType: "User", reference: "reviewer@example.com", wantType: constants.ApprovalApproverTypeUser, wantReferences: []string{"reviewer@example.com"}},
		{name: "queue", approverType: "Queue", reference: "Deal_Queue", wantType: constants.ApprovalApproverTypeQueue, wantReferences: []string{"Deal_Queue"}},
		{name: "role", approverType: "Role", reference: "Deal_Reviewer", wantType: constants.ApprovalApproverTypeRole, wantReferences: []string{"Deal_Reviewer"}},
		{name: "hierarchy", approverType: "Hierarchy", wantType: constants.ApprovalApproverTypeHierarchy},
		{name: "user hierarchy field", approverType: "UserHierarchyField", wantType: constants.ApprovalApproverTypeUserHierarchyField},
		{name: "related user field", approverType: "RelatedUser", reference: "Owner.ManagerId", wantType: constants.ApprovalApproverTypeRelatedUserField, wantRelated: "Owner.ManagerId", wantReferences: []string{"Owner.ManagerId"}},
		{name: "previous approver", approverType: "PreviousApprover", wantType: constants.ApprovalApproverTypePreviousApprover},
		{name: "ad hoc", approverType: "Adhoc", wantType: constants.ApprovalApproverTypeAdhoc},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			node, err := projectApprovalProcessNode(process, ApprovalProcessStep{
				Name: "Review", Approvers: []ApprovalProcessApprover{{Type: test.approverType, Name: test.reference}},
			}, 1)
			if err != nil {
				t.Fatal(err)
			}
			if node.ApproverType != test.wantType || !reflect.DeepEqual(node.ApproverReferences, test.wantReferences) {
				t.Fatalf("approver projection = %#v", node)
			}
			values := approvalProcessManagedNodeValues("definition", node)
			if values["ApproverType"] != test.wantType || values["ApproverUserId"] != nil {
				t.Fatalf("typed approver values = %#v", values)
			}
			if test.wantRelated == "" && values["ApproverRelatedField"] != nil || test.wantRelated != "" && values["ApproverRelatedField"] != test.wantRelated {
				t.Fatalf("related approver value = %#v", values["ApproverRelatedField"])
			}
		})
	}
}

func TestProjectApprovalProcessNodeRejectsUnsafeApproverShapes(t *testing.T) {
	process := ApprovalProcessBusinessKey{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval"}
	for name, approvers := range map[string][]ApprovalProcessApprover{
		"unknown type":             {{Type: "group", Name: "Reviewers"}},
		"user without name":        {{Type: "user"}},
		"queue without name":       {{Type: "queue"}},
		"role without name":        {{Type: "role"}},
		"related without field":    {{Type: "relatedUserField"}},
		"hierarchy with reference": {{Type: "hierarchy", Name: "ManagerId"}},
		"user hierarchy reference": {{Type: "userHierarchyField", Name: "ManagerId"}},
		"previous with reference":  {{Type: "previousApprover", Name: "005-unverified"}},
		"adhoc with reference":     {{Type: "adhoc", Name: "005-unverified"}},
		"multiple queues":          {{Type: "queue", Name: "One"}, {Type: "queue", Name: "Two"}},
		"mixed types":              {{Type: "user", Name: "one@example.com"}, {Type: "queue", Name: "Reviewers"}},
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := projectApprovalProcessNode(process, ApprovalProcessStep{Name: "Review", Approvers: approvers}, 1); err == nil {
				t.Fatal("projectApprovalProcessNode returned nil error")
			}
		})
	}
}

func TestApprovalProcessPersistencePlanRejectsMissingNodeActionReference(t *testing.T) {
	process := ApprovalProcessBusinessKey{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval"}
	plan := ApprovalProcessPersistencePlan{
		ParentDisposition:        ApprovalProcessParentManagedUpsert,
		Parent:                   ApprovalProcessParentRowIntent{BusinessKey: process, OwnerPack: "sales"},
		SoftDeleteMissingNodes:   true,
		ReplaceProcessActionRows: true,
		Nodes: []ApprovalProcessNodeRowIntent{{
			BusinessKey: ApprovalProcessNodeBusinessKey{Process: process, DeveloperName: "Manager"},
		}},
		Actions: []ApprovalProcessActionRowIntent{{
			Name: "Broken", ProcessNode: &ApprovalProcessNodeBusinessKey{Process: process, DeveloperName: "Missing"},
		}},
	}
	err := plan.Validate()
	if err == nil || !strings.Contains(err.Error(), "references missing ProcessNode Missing") {
		t.Fatalf("Validate error = %v, want missing-node failure", err)
	}
}

func TestApprovalProcessPersistencePlanRejectsAmbiguousNodes(t *testing.T) {
	process := ApprovalProcessBusinessKey{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval"}
	base := ApprovalProcessNodeRowIntent{BusinessKey: ApprovalProcessNodeBusinessKey{Process: process, DeveloperName: "Manager"}, AllowComments: true}
	for name, nodes := range map[string][]ApprovalProcessNodeRowIntent{
		"duplicate":                     {base, {BusinessKey: ApprovalProcessNodeBusinessKey{Process: process, DeveloperName: "manager"}, AllowComments: true}},
		"comments":                      {{BusinessKey: base.BusinessKey, RequireComments: true}},
		"reference without type":        {{BusinessKey: base.BusinessKey, ApproverReferences: []string{"reviewer@example.com"}, AllowComments: true}},
		"missing user reference":        {{BusinessKey: base.BusinessKey, ApproverType: constants.ApprovalApproverTypeUser, AllowComments: true}},
		"missing related reference":     {{BusinessKey: base.BusinessKey, ApproverType: constants.ApprovalApproverTypeRelatedUserField, AllowComments: true}},
		"multiple queue references":     {{BusinessKey: base.BusinessKey, ApproverType: constants.ApprovalApproverTypeQueue, ApproverReferences: []string{"One", "Two"}, AllowComments: true}},
		"forbidden hierarchy reference": {{BusinessKey: base.BusinessKey, ApproverType: constants.ApprovalApproverTypeHierarchy, ApproverReferences: []string{"ManagerId"}, AllowComments: true}},
		"noncanonical reference":        {{BusinessKey: base.BusinessKey, ApproverType: constants.ApprovalApproverTypeUser, ApproverReferences: []string{" reviewer@example.com "}, AllowComments: true}},
	} {
		t.Run(name, func(t *testing.T) {
			if err := (ApprovalProcessPersistencePlan{
				ParentDisposition:        ApprovalProcessParentManagedUpsert,
				Parent:                   ApprovalProcessParentRowIntent{BusinessKey: process, OwnerPack: "sales"},
				SoftDeleteMissingNodes:   true,
				ReplaceProcessActionRows: true,
				Nodes:                    nodes,
			}).Validate(); err == nil {
				t.Fatal("Validate returned nil error")
			}
		})
	}
}

func TestApprovalProcessPersistencePlanRejectsReconcileIntentTampering(t *testing.T) {
	process := ApprovalProcessBusinessKey{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval"}
	for name, tamper := range map[string]func(*ApprovalProcessPersistencePlan){
		"retain missing nodes": func(plan *ApprovalProcessPersistencePlan) { plan.SoftDeleteMissingNodes = false },
		"append action rows":   func(plan *ApprovalProcessPersistencePlan) { plan.ReplaceProcessActionRows = false },
	} {
		t.Run(name, func(t *testing.T) {
			plan := ApprovalProcessPersistencePlan{
				ParentDisposition:        ApprovalProcessParentManagedUpsert,
				Parent:                   ApprovalProcessParentRowIntent{BusinessKey: process, OwnerPack: "sales"},
				SoftDeleteMissingNodes:   true,
				ReplaceProcessActionRows: true,
			}
			tamper(&plan)
			if err := plan.Validate(); err == nil || !strings.Contains(err.Error(), "must soft-delete missing nodes and replace process action rows") {
				t.Fatalf("Validate error = %v", err)
			}
		})
	}
}

func TestPlanApprovalProcessPersistenceRejectsUnstableNodeKeys(t *testing.T) {
	base := ApprovalProcessProjectionRequest{
		ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval", ParentDisposition: ApprovalProcessParentManagedUpsert,
		OwnerPack: "crm-standard", RawMetadata: "raw",
	}
	for name, steps := range map[string][]ApprovalProcessStep{
		"missing":   {{Label: "No developer name"}},
		"invalid":   {{Name: "Bad-Step"}},
		"duplicate": {{Name: "Manager"}, {Name: " manager "}},
	} {
		t.Run(name, func(t *testing.T) {
			request := base
			request.Metadata = &ApprovalProcessMetadata{Steps: steps}
			if _, err := PlanApprovalProcessPersistence(request); err == nil {
				t.Fatal("PlanApprovalProcessPersistence returned nil error")
			}
		})
	}
}

func TestPlanApprovalProcessPersistenceRequiresExplicitParentDisposition(t *testing.T) {
	base := ApprovalProcessProjectionRequest{
		ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval", RawMetadata: "raw", Metadata: &ApprovalProcessMetadata{},
	}
	for _, test := range []struct {
		name                string
		disposition         ApprovalProcessParentDisposition
		ownerPack, parentID string
		wantError           bool
	}{
		{name: "managed", disposition: ApprovalProcessParentManagedUpsert, ownerPack: "crm-standard"},
		{name: "pre-applied ownerless", disposition: ApprovalProcessParentPreAppliedOwnerless, parentID: "04aPD"},
		{name: "missing disposition", wantError: true},
		{name: "managed missing owner", disposition: ApprovalProcessParentManagedUpsert, wantError: true},
		{name: "managed noncanonical owner", disposition: ApprovalProcessParentManagedUpsert, ownerPack: " crm-standard ", wantError: true},
		{name: "managed with parent id", disposition: ApprovalProcessParentManagedUpsert, ownerPack: "crm-standard", parentID: "04aPD", wantError: true},
		{name: "ownerless with owner", disposition: ApprovalProcessParentPreAppliedOwnerless, ownerPack: "crm-standard", parentID: "04aPD", wantError: true},
		{name: "ownerless missing parent id", disposition: ApprovalProcessParentPreAppliedOwnerless, wantError: true},
		{name: "ownerless noncanonical parent id", disposition: ApprovalProcessParentPreAppliedOwnerless, parentID: " 04aPD ", wantError: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			request := base
			request.ParentDisposition, request.OwnerPack, request.PreAppliedParentID = test.disposition, test.ownerPack, test.parentID
			plan, err := PlanApprovalProcessPersistence(request)
			if test.wantError {
				if err == nil {
					t.Fatalf("PlanApprovalProcessPersistence returned %#v, want error", plan)
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			if plan.ParentDisposition != test.disposition || plan.PreAppliedParentID != test.parentID || plan.Parent.OwnerPack != test.ownerPack {
				t.Fatalf("parent target = (%q,%q,%q)", plan.ParentDisposition, plan.PreAppliedParentID, plan.Parent.OwnerPack)
			}
		})
	}
}

func TestApprovalProcessProjectionDispositionsShareCanonicalChildIntent(t *testing.T) {
	raw := `<ApprovalProcess><approvalStep><name>Review</name><assignedApprover><approver><type>User</type><name>one@example.com</name></approver><approver><type>User</type><name>two@example.com</name></approver></assignedApprover><stepApprovalActions><actions><name>Notify</name><type>Flow</type><apiName>Notify</apiName></actions></stepApprovalActions></approvalStep></ApprovalProcess>`
	metadata, err := DecodeApprovalProcessMetadata(raw)
	if err != nil {
		t.Fatal(err)
	}
	base := ApprovalProcessProjectionRequest{ObjectAPIName: "Opportunity", DeveloperName: "Deal_Approval", RawMetadata: raw, Metadata: metadata}
	managedRequest := base
	managedRequest.ParentDisposition, managedRequest.OwnerPack = ApprovalProcessParentManagedUpsert, "crm-standard"
	managed, err := PlanApprovalProcessPersistence(managedRequest)
	if err != nil {
		t.Fatal(err)
	}
	ownerlessRequest := base
	ownerlessRequest.ParentDisposition, ownerlessRequest.PreAppliedParentID = ApprovalProcessParentPreAppliedOwnerless, "04aPD"
	ownerless, err := PlanApprovalProcessPersistence(ownerlessRequest)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(managed.Nodes, ownerless.Nodes) || !reflect.DeepEqual(managed.Actions, ownerless.Actions) {
		t.Fatalf("child intents differ: managed %#v/%#v, ownerless %#v/%#v", managed.Nodes, managed.Actions, ownerless.Nodes, ownerless.Actions)
	}
	managedParent := managed.Parent
	managedParent.OwnerPack = ""
	if !reflect.DeepEqual(managedParent, ownerless.Parent) {
		t.Fatalf("parent facts differ beyond ownership: managed %#v, ownerless %#v", managed.Parent, ownerless.Parent)
	}
}
