package dialectsfcrm

import (
	"encoding/base64"
	"encoding/json"
	"strings"
	"testing"

	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
)

func TestCompileApprovalPublicationSeedsLowersCompleteLifecycle(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	bundleRaw, digest, err := CompileApprovalPublicationSeeds(
		"sha256:"+strings.Repeat("a", 64), raw,
	)
	if err != nil {
		t.Fatal(err)
	}
	bundle, decodedDigest, err := approvalir.DecodePublicationBundle(bundleRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	if decodedDigest != digest || len(bundle.Definitions) != 1 {
		t.Fatalf("bundle = %#v, digest = %q, want %q", bundle, decodedDigest, digest)
	}
	definition := bundle.Definitions[0]
	if definition.DefinitionID != "l2.approval.WorkItem.Review" ||
		definition.ObjectID != "WorkItem" || definition.DefinitionName != "Review" ||
		len(definition.Envelope.Definition.Steps) != 1 ||
		len(definition.Envelope.Definition.InitialSubmissionActions) != 1 ||
		len(definition.Envelope.Definition.FinalApprovalActions) != 1 {
		t.Fatalf("published definition = %#v", definition)
	}
}

func TestCompileApprovalPublicationSeedsRejectsDanglingActionNode(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	seed.ProcessActions[0].ProcessNodeID = "missing-node"
	seed.ProcessActions[0].ActionEvent = "StepApproval"
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw); err == nil ||
		!strings.Contains(err.Error(), "dangling node") {
		t.Fatalf("CompileApprovalPublicationSeeds(dangling node) error = %v", err)
	}
}

func TestCompileApprovalPublicationSeedsUsesStoppedProcessActionsNotStaleMetadataReferences(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	metadata := `<ApprovalProcess>` +
		`<approvalStep><name>Manager</name><label>Manager</label><assignedApprover><approver><name>OwnerId</name><type>relatedUserField</type></approver></assignedApprover><rejectBehavior><type>RejectRequest</type></rejectBehavior></approvalStep>` +
		`<initialSubmissionActions><action><name>Missing_Action</name><type>FieldUpdate</type></action></initialSubmissionActions>` +
		`</ApprovalProcess>`
	seed.ProcessDefinitions[0].MetadataBase64 = base64.StdEncoding.EncodeToString([]byte(metadata))
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	bundleRaw, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw)
	if err != nil {
		t.Fatal(err)
	}
	bundle, _, err := approvalir.DecodePublicationBundle(bundleRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	definition := bundle.Definitions[0].Envelope.Definition
	if len(definition.InitialSubmissionActions) != 1 ||
		definition.InitialSubmissionActions[0].Reference != nil ||
		definition.InitialSubmissionActions[0].FieldUpdate == nil {
		t.Fatalf("initial actions = %#v, want only the concrete stopped ProcessAction row", definition.InitialSubmissionActions)
	}
}

func TestCompileApprovalPublicationSeedsAcceptsBothEntryFilterShapes(t *testing.T) {
	for name, entryFilter := range map[string]json.RawMessage{
		"legacy string":   json.RawMessage(`"Amount greaterThan 1000"`),
		"criteria object": json.RawMessage(`{"booleanFilter":"1","criteriaItems":[{"field":"Amount","operation":"greaterThan","value":"1000"}]}`),
	} {
		t.Run(name, func(t *testing.T) {
			seed := approvalPublicationSeedFixture(t)
			seed.ProcessDefinitions[0].EntryFilter = entryFilter
			raw, err := json.Marshal(seed)
			if err != nil {
				t.Fatal(err)
			}
			bundleRaw, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw)
			if err != nil {
				t.Fatal(err)
			}
			bundle, _, err := approvalir.DecodePublicationBundle(bundleRaw, nil)
			if err != nil {
				t.Fatal(err)
			}
			criteria := bundle.Definitions[0].Envelope.Definition.EntryCriteria
			if criteria == nil || len(criteria.Items) != 1 || criteria.Items[0].Field != "Amount" {
				t.Fatalf("entry criteria = %#v", criteria)
			}
		})
	}
}

func TestCompileApprovalPublicationSeedsAcceptsEmptyLegacyEntryFilter(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	seed.ProcessDefinitions[0].EntryFilter = json.RawMessage(`""`)
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw); err != nil {
		t.Fatal(err)
	}
}

func approvalPublicationSeedFixture(t *testing.T) ApprovalPublicationSeed {
	t.Helper()
	metadata := `{"approvalStep":[{"name":"Manager","label":"Manager","assignedApprover":{"approver":{"name":"OwnerId","type":"relatedUserField"}},"rejectBehavior":{"type":"RejectRequest"}}]}`
	return ApprovalPublicationSeed{
		ProcessDefinitions: []ApprovalPublicationProcessDefinition{{
			ID: "definition-1", DeveloperName: "Review", Name: "Review", TableEnumOrID: "WorkItem",
			State: "Active", IsActive: true, AllowRecall: true,
			MetadataBase64: base64.StdEncoding.EncodeToString([]byte(metadata)),
		}},
		ProcessNodes: []ApprovalPublicationProcessNode{{
			ID: "node-1", DeveloperName: "Manager", Name: "Manager", ProcessDefinitionID: "definition-1",
			ApproverType: "relatedUserField", ApproverRelatedField: "OwnerId", StepOrder: 1,
		}},
		ProcessActions: []ApprovalPublicationProcessAction{
			{ID: "action-1", Name: "Set pending", ProcessDefinitionID: "definition-1", ActionType: "FieldUpdate",
				ActionEvent: "Submit", FieldAPIName: "Status", FieldValue: "Pending", Order: 1},
			{ID: "action-2", Name: "Set approved", ProcessDefinitionID: "definition-1", ActionType: "FieldUpdate",
				ActionEvent: "FinalApproval", FieldAPIName: "Status", FieldValue: "Approved", Order: 1},
		},
	}
}

func TestCompileApprovalPublicationSeedsKeepsSameDeveloperNameParentsIsolated(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	second := seed.ProcessDefinitions[0]
	second.ID = "definition-2"
	second.TableEnumOrID = "Case"
	seed.ProcessDefinitions = append(seed.ProcessDefinitions, second)
	seed.ProcessNodes = append(seed.ProcessNodes, ApprovalPublicationProcessNode{
		ID: "node-2", DeveloperName: "Manager", Name: "Manager", ProcessDefinitionID: second.ID,
		ApproverType: "relatedUserField", ApproverRelatedField: "CreatedById", StepOrder: 1,
	})
	seed.ProcessActions = append(seed.ProcessActions, ApprovalPublicationProcessAction{
		ID: "action-3", Name: "Set case pending", ProcessDefinitionID: second.ID,
		ActionType: "FieldUpdate", ActionEvent: "Submit", FieldAPIName: "Status", FieldValue: "New", Order: 1,
	})
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	bundleRaw, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw)
	if err != nil {
		t.Fatal(err)
	}
	bundle, _, err := approvalir.DecodePublicationBundle(bundleRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(bundle.Definitions) != 2 || bundle.Definitions[0].ObjectID == bundle.Definitions[1].ObjectID {
		t.Fatalf("published definitions = %#v", bundle.Definitions)
	}
}

func TestCompileApprovalPublicationSeedsNormalizesUniqueLegacyParentName(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	stableRaw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	stableBundle, stableDigest, err := CompileApprovalPublicationSeeds(
		"sha256:"+strings.Repeat("a", 64), stableRaw,
	)
	if err != nil {
		t.Fatal(err)
	}
	seed.ProcessNodes[0].ProcessDefinitionID = ""
	seed.ProcessNodes[0].ProcessDeveloperName = seed.ProcessDefinitions[0].DeveloperName
	for index := range seed.ProcessActions {
		seed.ProcessActions[index].ProcessDefinitionID = ""
		seed.ProcessActions[index].ProcessDeveloperName = seed.ProcessDefinitions[0].DeveloperName
	}
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	bundleRaw, digest, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw)
	if err != nil {
		t.Fatal(err)
	}
	if digest != stableDigest || string(bundleRaw) != string(stableBundle) {
		t.Fatal("unique legacy parent names did not normalize to the stable-ID canonical bundle")
	}
	bundle, _, err := approvalir.DecodePublicationBundle(bundleRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(bundle.Definitions) != 1 || bundle.Definitions[0].DefinitionID != "l2.approval.WorkItem.Review" {
		t.Fatalf("published definitions = %#v", bundle.Definitions)
	}
}

func TestCompileApprovalPublicationSeedsRejectsAmbiguousLegacyParentName(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	second := seed.ProcessDefinitions[0]
	second.ID = "definition-2"
	second.TableEnumOrID = "Case"
	seed.ProcessDefinitions = append(seed.ProcessDefinitions, second)
	seed.ProcessNodes[0].ProcessDefinitionID = ""
	seed.ProcessNodes[0].ProcessDeveloperName = seed.ProcessDefinitions[0].DeveloperName
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw); err == nil ||
		!strings.Contains(err.Error(), "ambiguous process developer name") {
		t.Fatalf("CompileApprovalPublicationSeeds(ambiguous legacy parent) error = %v", err)
	}
}

func TestCompileApprovalPublicationSeedsRejectsConflictingParentIdentity(t *testing.T) {
	seed := approvalPublicationSeedFixture(t)
	seed.ProcessNodes[0].ProcessDeveloperName = "OtherReview"
	raw, err := json.Marshal(seed)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw); err == nil ||
		!strings.Contains(err.Error(), "conflicting process identities") {
		t.Fatalf("CompileApprovalPublicationSeeds(conflicting parent identity) error = %v", err)
	}
}

func TestCompileApprovalPublicationSeedsRejectsAmbiguousChildGraph(t *testing.T) {
	for name, mutate := range map[string]func(*ApprovalPublicationSeed){
		"duplicate node developer name": func(seed *ApprovalPublicationSeed) {
			duplicate := seed.ProcessNodes[0]
			duplicate.ID = "node-2"
			duplicate.DeveloperName = "manager"
			seed.ProcessNodes = append(seed.ProcessNodes, duplicate)
		},
		"duplicate action id": func(seed *ApprovalPublicationSeed) {
			duplicate := seed.ProcessActions[0]
			seed.ProcessActions = append(seed.ProcessActions, duplicate)
		},
		"duplicate action semantic identity": func(seed *ApprovalPublicationSeed) {
			duplicate := seed.ProcessActions[0]
			duplicate.ID = "action-3"
			seed.ProcessActions = append(seed.ProcessActions, duplicate)
		},
		"cross-definition node": func(seed *ApprovalPublicationSeed) {
			second := seed.ProcessDefinitions[0]
			second.ID = "definition-2"
			second.DeveloperName = "OtherReview"
			seed.ProcessDefinitions = append(seed.ProcessDefinitions, second)
			seed.ProcessActions[0].ProcessDefinitionID = second.ID
			seed.ProcessActions[0].ProcessNodeID = seed.ProcessNodes[0].ID
		},
	} {
		t.Run(name, func(t *testing.T) {
			seed := approvalPublicationSeedFixture(t)
			mutate(&seed)
			raw, err := json.Marshal(seed)
			if err != nil {
				t.Fatal(err)
			}
			if _, _, err := CompileApprovalPublicationSeeds("sha256:"+strings.Repeat("a", 64), raw); err == nil {
				t.Fatal("CompileApprovalPublicationSeeds accepted an ambiguous child graph")
			}
		})
	}
}
