package catalogreconcile

import (
	"errors"
	"strings"
	"testing"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/approvalv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
)

func TestBindApprovalPublicationRequiresExactBundleAndL2Identity(t *testing.T) {
	metadataDigest := "sha256:" + strings.Repeat("a", 64)
	envelopeRaw, err := approvalir.EncodeDefinitionEnvelope([]byte("L2 source"), &approvalir.Definition{
		Active: true,
		Policy: approvalir.ApprovalPolicy{RecordEditability: approvalir.RecordEditabilityAdminOnly,
			AllowedSubmitters: []approvalir.AllowedSubmitter{{Kind: approvalir.SubmitterKindAllInternalUsers}}},
		Steps: []approvalir.Step{{Name: "Review", Label: "Review",
			Approvers:       []approvalir.Approver{{Kind: approvalir.ApproverKindUser, Reference: "user-1"}},
			RejectBehavior:  approvalir.RejectBehaviorFinalRejection,
			ApprovalActions: []approvalir.Action{}, RejectionActions: []approvalir.Action{}}},
		InitialSubmissionActions: []approvalir.Action{}, StepApprovalActions: []approvalir.Action{},
		FinalApprovalActions: []approvalir.Action{}, FinalRejectionActions: []approvalir.Action{},
		RecallActions: []approvalir.Action{}, ReassignActions: []approvalir.Action{},
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	envelope, err := approvalir.DecodeDefinitionEnvelope(envelopeRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	bundleRaw, bundleDigest, err := approvalir.EncodePublicationBundle(approvalir.PublicationBundle{
		Schema: approvalir.PublicationBundleSchemaV1, SourceDigest: metadataDigest,
		Definitions: []approvalir.PublishedDefinition{{DefinitionID: "l2.WorkItem.Review",
			ObjectID: "WorkItem", DefinitionName: "Review", Envelope: *envelope}},
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	config := Config{Schema: ConfigSchemaV4, MetadataSourceDigest: metadataDigest,
		ApprovalPublication: &ApprovalPublicationConfig{BundlePath: "/run/approval.json", ExpectedDigest: bundleDigest}}
	request := Request{MetadataSourceDigest: catalogsnapshot.Digest(metadataDigest),
		Capabilities: []CapabilitySelection{{RuntimeCapabilityID: approvalv1.ExecuteCapabilityName}}}
	bound, err := config.BindApprovalPublication(request, bundleRaw)
	if err != nil {
		t.Fatal(err)
	}
	if bound.approvalPublication == nil || bound.approvalPublication.digest != catalogsnapshot.Digest(bundleDigest) ||
		len(bound.approvalPublication.bundle.Definitions) != 1 {
		t.Fatalf("bound Approval publication = %#v", bound.approvalPublication)
	}

	wrongDigest := config
	wrongDigest.ApprovalPublication = &ApprovalPublicationConfig{BundlePath: "/run/approval.json", ExpectedDigest: "sha256:" + strings.Repeat("b", 64)}
	if _, err := wrongDigest.BindApprovalPublication(request, bundleRaw); err == nil || !strings.Contains(err.Error(), "does not match expected") {
		t.Fatalf("wrong bundle digest error = %v", err)
	}
	wrongSource := request
	wrongSource.MetadataSourceDigest = catalogsnapshot.Digest("sha256:" + strings.Repeat("c", 64))
	if _, err := config.BindApprovalPublication(wrongSource, bundleRaw); err == nil || !strings.Contains(err.Error(), "metadata source digest") {
		t.Fatalf("wrong metadata source error = %v", err)
	}
}

func TestApprovalPublicationIdentityIsBoundToOperationRetry(t *testing.T) {
	metadataDigest := "sha256:" + strings.Repeat("a", 64)
	bundleA, digestA := approvalPublicationBundleFixture(t, metadataDigest, "l2.WorkItem.Review", "Review")
	bundleB, digestB := approvalPublicationBundleFixture(t, metadataDigest, "l2.WorkItem.SecondReview", "SecondReview")
	request := fixtureReconcileRequest()
	request.AppCapabilityID, request.HandlerRef, request.RuntimeCapabilityID = "", "", ""
	request.MetadataSourceDigest = catalogsnapshot.Digest(metadataDigest)
	request.Capabilities = []CapabilitySelection{
		{
			AppCapabilityID:     "io.nexuscrm.sfcrm.approval.execute.v1",
			HandlerRef:          "io.nexuscrm.sfcrm.handler.approval.execute.v1",
			RuntimeCapabilityID: approvalv1.ExecuteCapabilityName,
		},
		{
			AppCapabilityID:     "io.nexuscrm.sfcrm.ui.record-access.v1",
			HandlerRef:          "io.nexuscrm.sfcrm.handler.ui.record-access.v1",
			RuntimeCapabilityID: recordaccessv1.CapabilityName,
		},
	}
	request.Operation.GrantedCapabilities = []string{
		approvalv1.ExecuteCapabilityName,
		recordaccessv1.CapabilityName,
	}

	configA := Config{
		Schema: ConfigSchemaV4, MetadataSourceDigest: metadataDigest,
		ApprovalPublication: &ApprovalPublicationConfig{BundlePath: "/run/approval-a.json", ExpectedDigest: digestA},
	}
	boundA, err := configA.BindApprovalPublication(request, bundleA)
	if err != nil {
		t.Fatal(err)
	}
	closedA, err := bindContinuationIntent(boundA)
	if err != nil {
		t.Fatal(err)
	}
	closedAgain, err := bindContinuationIntent(boundA)
	if err != nil || closedAgain.Operation.ContinuationIntentDigest != closedA.Operation.ContinuationIntentDigest {
		t.Fatalf("same Approval publication identity changed: digest=%q err=%v", closedAgain.Operation.ContinuationIntentDigest, err)
	}

	configB := configA
	configB.ApprovalPublication = &ApprovalPublicationConfig{BundlePath: "/run/approval-b.json", ExpectedDigest: digestB}
	boundB, err := configB.BindApprovalPublication(request, bundleB)
	if err != nil {
		t.Fatal(err)
	}
	closedB, err := bindContinuationIntent(boundB)
	if err != nil {
		t.Fatal(err)
	}
	if closedB.Operation.ContinuationIntentDigest == closedA.Operation.ContinuationIntentDigest {
		t.Fatal("different Approval publications produced the same continuation identity")
	}

	authority := &fakeAuthority{operation: catalogapp.Operation{OperationID: "operation-test"}}
	if _, err := authority.BeginOperation(t.Context(), closedA.Operation); err != nil {
		t.Fatal(err)
	}
	if _, err := authority.BeginOperation(t.Context(), closedA.Operation); err != nil {
		t.Fatalf("same Approval publication retry failed: %v", err)
	}
	if _, err := authority.BeginOperation(t.Context(), closedB.Operation); !errors.Is(err, catalogapp.ErrConflict) {
		t.Fatalf("changed Approval publication retry error = %v, want conflict", err)
	}

	corrupt := boundA
	corrupt.approvalPublication = &approvalPublicationSelection{
		bundle: boundA.approvalPublication.bundle,
		digest: catalogsnapshot.Digest("sha256:" + strings.Repeat("f", 64)),
	}
	if _, err := bindContinuationIntent(corrupt); err == nil || !strings.Contains(err.Error(), "content and immutable digest disagree") {
		t.Fatalf("corrupt Approval publication identity error = %v", err)
	}
}

func approvalPublicationBundleFixture(
	t *testing.T,
	metadataDigest string,
	definitionID string,
	definitionName string,
) ([]byte, string) {
	t.Helper()
	envelopeRaw, err := approvalir.EncodeDefinitionEnvelope([]byte(definitionID), &approvalir.Definition{
		Active: true,
		Policy: approvalir.ApprovalPolicy{
			RecordEditability: approvalir.RecordEditabilityAdminOnly,
			AllowedSubmitters: []approvalir.AllowedSubmitter{{Kind: approvalir.SubmitterKindAllInternalUsers}},
		},
		Steps: []approvalir.Step{{
			Name: "Review", Label: "Review",
			Approvers:      []approvalir.Approver{{Kind: approvalir.ApproverKindUser, Reference: "user-1"}},
			RejectBehavior: approvalir.RejectBehaviorFinalRejection,
		}},
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	envelope, err := approvalir.DecodeDefinitionEnvelope(envelopeRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	raw, digest, err := approvalir.EncodePublicationBundle(approvalir.PublicationBundle{
		Schema: approvalir.PublicationBundleSchemaV1, SourceDigest: metadataDigest,
		Definitions: []approvalir.PublishedDefinition{{
			DefinitionID: definitionID, ObjectID: "WorkItem", DefinitionName: definitionName,
			Envelope: *envelope,
		}},
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	return raw, digest
}

func TestBindApprovalPublicationRejectsUnresolvedActionsBeforeReconcile(t *testing.T) {
	metadataDigest := "sha256:" + strings.Repeat("a", 64)
	envelopeRaw, err := approvalir.EncodeDefinitionEnvelope([]byte("L2 source"), &approvalir.Definition{
		Active: true,
		Policy: approvalir.ApprovalPolicy{RecordEditability: approvalir.RecordEditabilityAdminOnly,
			AllowedSubmitters: []approvalir.AllowedSubmitter{{Kind: approvalir.SubmitterKindAllInternalUsers}}},
		Steps: []approvalir.Step{{Name: "Review", Label: "Review",
			Approvers:      []approvalir.Approver{{Kind: approvalir.ApproverKindUser, Reference: "user-1"}},
			RejectBehavior: approvalir.RejectBehaviorFinalRejection}},
		InitialSubmissionActions: []approvalir.Action{{Name: "Missing", Kind: approvalir.ActionKindFieldUpdate,
			Reference: &approvalir.ActionReference{APIName: "Missing_Action"}}},
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	envelope, err := approvalir.DecodeDefinitionEnvelope(envelopeRaw, nil)
	if err != nil {
		t.Fatal(err)
	}
	bundleRaw, bundleDigest, err := approvalir.EncodePublicationBundle(approvalir.PublicationBundle{
		Schema: approvalir.PublicationBundleSchemaV1, SourceDigest: metadataDigest,
		Definitions: []approvalir.PublishedDefinition{{DefinitionID: "l2.WorkItem.Review",
			ObjectID: "WorkItem", DefinitionName: "Review", Envelope: *envelope}},
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	config := Config{Schema: ConfigSchemaV4, MetadataSourceDigest: metadataDigest,
		ApprovalPublication: &ApprovalPublicationConfig{BundlePath: "/run/approval.json", ExpectedDigest: bundleDigest}}
	request := Request{MetadataSourceDigest: catalogsnapshot.Digest(metadataDigest),
		Capabilities: []CapabilitySelection{{RuntimeCapabilityID: approvalv1.ExecuteCapabilityName}}}
	if _, err := config.BindApprovalPublication(request, bundleRaw); err == nil ||
		!strings.Contains(err.Error(), `unresolved action reference "Missing_Action"`) {
		t.Fatalf("BindApprovalPublication(unresolved action) error = %v", err)
	}
}
