package approvalir

import (
	"strings"
	"testing"
)

func TestDefinitionValidateAcceptsResolvedAggregate(t *testing.T) {
	if err := validDefinitionAggregate().Validate(); err != nil {
		t.Fatalf("Validate() error = %v", err)
	}
}

func TestDefinitionValidateAllowsMissingRootLabel(t *testing.T) {
	definition := validDefinitionAggregate()
	definition.Label = " \t"
	if err := definition.Validate(); err != nil {
		t.Fatalf("Validate() error = %v", err)
	}
}

func TestDefinitionEnumsAreClosed(t *testing.T) {
	tests := []struct {
		name           string
		valid, invalid bool
	}{
		{"approver kind", ApproverKindHierarchy.Valid(), ApproverKind("dialect").Valid()},
		{"multiple mode", MultipleApproverModeUnanimous.Valid(), MultipleApproverMode("all").Valid()},
		{"reject behavior", RejectBehaviorFinalRejection.Valid(), RejectBehavior("stop").Valid()},
		{"criteria transition", IfCriteriaNotMetReject.Valid(), IfCriteriaNotMet("skip").Valid()},
		{"hierarchy initial principal", HierarchyInitialPrincipalSubmitter.Valid(), HierarchyInitialPrincipal("user").Valid()},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if !test.valid || test.invalid {
				t.Fatalf("closed enum results = (%v, %v)", test.valid, test.invalid)
			}
		})
	}
}

func TestDefinitionValidateRejectsInvalidAggregate(t *testing.T) {
	falseValue, trueValue := false, true
	tests := []struct {
		name    string
		mutate  func(*Definition)
		wantErr string
	}{
		{"invalid policy", func(d *Definition) { d.Policy.RecordEditability = "" }, "policy:"},
		{"nonpositive order", func(d *Definition) { zero := 0; d.ProcessOrder = &zero }, "process order must be positive"},
		{"invalid entry criteria", func(d *Definition) { d.EntryCriteria = &Criteria{} }, "entry criteria:"},
		{"missing hierarchy field", func(d *Definition) { d.HierarchyRouting.UserHierarchyField = " " }, "user hierarchy field is required"},
		{"invalid hierarchy principal", func(d *Definition) { d.HierarchyRouting.InitialPrincipal = "actor" }, "invalid initial principal"},
		{"active without steps", func(d *Definition) { d.Steps = nil }, "requires at least one step"},
		{"blank step name", func(d *Definition) { d.Steps[0].Name = " " }, "name is required"},
		{"duplicate normalized name", func(d *Definition) { step := d.Steps[0]; step.Name = " REVIEW "; d.Steps = append(d.Steps, step) }, "duplicate normalized name"},
		{"blank step label", func(d *Definition) { d.Steps[0].Label = "\t" }, "label is required"},
		{"comments inconsistent", func(d *Definition) { d.Steps[0].AllowComments, d.Steps[0].RequireComments = &falseValue, &trueValue }, "comments cannot be required"},
		{"no approver", func(d *Definition) { d.Steps[0].Approvers = nil }, "at least one approver"},
		{"one approver with mode", func(d *Definition) { d.Steps[0].MultipleApproverMode = MultipleApproverModeUnanimous }, "must not carry"},
		{"multiple approvers without mode", func(d *Definition) { d.Steps[0].Approvers = append(d.Steps[0].Approvers, d.Steps[0].Approvers[0]) }, "require a valid"},
		{"unknown reject behavior", func(d *Definition) { d.Steps[0].RejectBehavior = "stop" }, "invalid reject behavior"},
		{"first step back to previous", func(d *Definition) { d.Steps[0].RejectBehavior = RejectBehaviorBackToPrevious }, "guaranteed prior actor"},
		{"criteria without transition", func(d *Definition) { d.Steps[0].Criteria = validCriteria() }, "specified together"},
		{"transition without criteria", func(d *Definition) { d.Steps[0].IfCriteriaNotMet = IfCriteriaNotMetApprove }, "specified together"},
		{"unknown criteria transition", func(d *Definition) { d.Steps[0].Criteria = validCriteria(); d.Steps[0].IfCriteriaNotMet = "continue" }, "invalid if-criteria-not-met"},
		{"invalid nested criteria", func(d *Definition) {
			d.Steps[0].Criteria = &Criteria{}
			d.Steps[0].IfCriteriaNotMet = IfCriteriaNotMetReject
		}, "criteria:"},
		{"unknown approver with reference", func(d *Definition) { d.Steps[0].Approvers[0] = Approver{Kind: "dialect", Reference: "x"} }, "invalid kind"},
		{"hierarchy with reference", func(d *Definition) { d.Steps[0].Approvers[0] = Approver{Kind: ApproverKindHierarchy, Reference: "x"} }, "must not carry"},
		{"hierarchy without routing", func(d *Definition) {
			d.HierarchyRouting = nil
			d.Steps[0].Approvers[0] = Approver{Kind: ApproverKindHierarchy}
		}, "requires definition hierarchy routing"},
		{"ad hoc with reference", func(d *Definition) { d.Steps[0].Approvers[0] = Approver{Kind: ApproverKindAdHoc, Reference: "x"} }, "must not carry"},
		{"previous with reference", func(d *Definition) {
			d.Steps[0].Approvers[0] = Approver{Kind: ApproverKindPreviousApprover, Reference: "x"}
		}, "must not carry"},
		{"previous on first step", func(d *Definition) { d.Steps[0].Approvers[0] = Approver{Kind: ApproverKindPreviousApprover} }, "guaranteed prior actor"},
		{"referenced kind without reference", func(d *Definition) { d.Steps[0].Approvers[0] = Approver{Kind: ApproverKindRole} }, "requires a reference"},
		{"invalid step action", func(d *Definition) { d.Steps[0].ApprovalActions = []Action{{Kind: ActionKindFlow}} }, "approval actions:"},
		{"invalid definition action", func(d *Definition) { d.ReassignActions = []Action{{Kind: ActionKindAlert}} }, "reassign actions:"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			definition := validDefinitionAggregate()
			test.mutate(definition)
			if err := definition.Validate(); err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Fatalf("Validate() error = %v, want containing %q", err, test.wantErr)
			}
		})
	}
}

func TestDefinitionValidateTracksPriorActorAcrossSkippedSteps(t *testing.T) {
	for _, behavior := range []IfCriteriaNotMet{IfCriteriaNotMetApprove, IfCriteriaNotMetReject} {
		t.Run(string(behavior), func(t *testing.T) {
			definition := definitionWithPreviousApprover()
			definition.Steps[0].Criteria = validCriteria()
			definition.Steps[0].IfCriteriaNotMet = behavior
			if err := definition.Validate(); err != nil {
				t.Fatalf("Validate() error = %v", err)
			}
		})
	}

	definition := definitionWithPreviousApprover()
	definition.Steps[0].Criteria = validCriteria()
	definition.Steps[0].IfCriteriaNotMet = IfCriteriaNotMetGotoNextStep
	if err := definition.Validate(); err == nil || !strings.Contains(err.Error(), "guaranteed prior actor") {
		t.Fatalf("skipped previous-approver error = %v", err)
	}
	definition.Steps[1].Approvers = []Approver{{Kind: ApproverKindUser, Reference: "reviewer"}}
	definition.Steps[1].RejectBehavior = RejectBehaviorBackToPrevious
	if err := definition.Validate(); err == nil || !strings.Contains(err.Error(), "guaranteed prior actor") {
		t.Fatalf("skipped back-to-previous error = %v", err)
	}
}

func validDefinitionAggregate() *Definition {
	order := 1
	return &Definition{
		Label: "Review", Active: true, ProcessOrder: &order,
		Policy: ApprovalPolicy{RecordEditability: RecordEditabilityAdminOnly},
		HierarchyRouting: &HierarchyRouting{
			UserHierarchyField: "manager", InitialPrincipal: HierarchyInitialPrincipalRecordOwner,
		},
		Steps: []Step{{
			Name: "review", Label: "Review", RejectBehavior: RejectBehaviorRejectRequest,
			Approvers: []Approver{{Kind: ApproverKindUser, Reference: "reviewer"}},
		}},
		InitialSubmissionActions: []Action{{Name: "notify", Kind: ActionKindFlow, Flow: &FlowAction{APIName: "notify"}}},
	}
}

func definitionWithPreviousApprover() *Definition {
	definition := validDefinitionAggregate()
	definition.Steps = append(definition.Steps, Step{
		Name: "final", Label: "Final", RejectBehavior: RejectBehaviorRejectRequest,
		Approvers: []Approver{{Kind: ApproverKindPreviousApprover}},
	})
	return definition
}

func validCriteria() *Criteria {
	return &Criteria{Mode: CriteriaModeItems, Items: []CriteriaItem{{
		Field: "amount", Operation: CriteriaOperatorGreaterThan, Value: "0",
	}}}
}
