package catalogsnapshot

import (
	"bytes"
	"encoding/json"
	"errors"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/referenceidentity"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
	approvalv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/approvalv1"
)

const (
	testApprovalDefinitionA = "approval.workitem.primary"
	testApprovalDefinitionB = "approval.workitem.secondary"
	testApprovalFlowName    = "ApprovalOnlyFlow"
)

func TestApprovalExecutionRuntimeViewCarriesFieldMappingValuePolicies(t *testing.T) {
	input := approvalCompileInput(t)
	for index := range input.MutationDefinitions {
		rule := input.MutationDefinitions[index].FieldMappingRule
		if rule == nil {
			continue
		}
		rule.ValueMap = `{"legacy":"Current"}`
		rule.OnUnmappedValue = connector.OnUnmappedValueSkip
	}
	snapshot := mustOpenSnapshot(t, mustCompile(t, input))
	executionArtifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA, CapabilityID: approvalv1.ExecuteCapabilityName,
	})
	if err != nil {
		t.Fatalf("project approval execution view: %v", err)
	}
	execution := mustOpenView(t, executionArtifact)
	if execution.doc.Schema != RuntimeMetadataViewSchemaV25 {
		t.Fatalf("approval execution schema = %q, want %q", execution.doc.Schema, RuntimeMetadataViewSchemaV25)
	}
	rules := execution.FieldMappingRulesForTarget("WorkItem")
	if len(rules) != 1 || rules[0].ValueMap != `{"legacy":"Current"}` ||
		rules[0].OnUnmappedValue != connector.OnUnmappedValueSkip {
		t.Fatalf("approval execution mapping policies = %#v", rules)
	}
	readArtifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA, CapabilityID: approvalv1.ReadCapabilityName,
	})
	if err != nil {
		t.Fatalf("project approval read view: %v", err)
	}
	read := mustOpenView(t, readArtifact)
	if read.doc.Schema != RuntimeMetadataViewSchemaV16 || read.doc.Mutation != nil {
		t.Fatalf("approval read inherited mutation policy: schema=%q mutation=%t", read.doc.Schema, read.doc.Mutation != nil)
	}
}

func TestApprovalRuntimeViewIsScopedImmutableRevisionBoundAndLeastPrivilege(t *testing.T) {
	input := approvalCompileInput(t)
	direct := mustCompile(t, input)
	effective, err := BuildEffectiveMetadata(input)
	if err != nil {
		t.Fatalf("BuildEffectiveMetadata() error = %v", err)
	}
	openedEffective, err := OpenEffectiveMetadata(effective.Digest(), effective.Bytes())
	if err != nil {
		t.Fatalf("OpenEffectiveMetadata() error = %v", err)
	}
	compiled, err := CompileEffectiveMetadata(openedEffective)
	if err != nil {
		t.Fatalf("CompileEffectiveMetadata() error = %v", err)
	}
	if !bytes.Equal(direct.Bytes(), compiled.Bytes()) || direct.Digest() != compiled.Digest() {
		t.Fatal("approval effective-metadata round trip changed the Catalog artifact")
	}
	snapshot := mustOpenSnapshot(t, compiled)
	artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA,
		CapabilityID:      approvalv1.ExecuteCapabilityName,
	})
	if err != nil {
		t.Fatalf("ProjectRuntimeMetadataView() error = %v", err)
	}
	view := mustOpenView(t, artifact)
	if view.doc.Schema != RuntimeMetadataViewSchemaV16 || view.Profile() != ProjectionApprovalExecutionV1 {
		t.Fatalf("approval Runtime view = schema %q profile %q", view.doc.Schema, view.Profile())
	}
	if !view.SupportsMutationClosure() {
		t.Fatal("approval execution view lost its compiler-validated mutation closure")
	}
	if view.Lineage().CatalogRevision != testRevision || view.SourceSnapshotDigest() != snapshot.Digest() {
		t.Fatalf("approval Runtime lineage = %#v source=%s", view.Lineage(), view.SourceSnapshotDigest())
	}
	if err := view.VerifySource(snapshot); err != nil {
		t.Fatalf("VerifySource() error = %v", err)
	}
	definitions := view.ActiveApprovalDefinitionsForObject("workitem")
	if len(definitions) != 2 || definitions[0].DefinitionID != testApprovalDefinitionB ||
		definitions[1].DefinitionID != testApprovalDefinitionA {
		t.Fatalf("ordered active approval definitions = %#v", definitions)
	}
	if objects := view.ActiveApprovalObjectAPINames(); len(objects) != 1 || objects[0] != "WorkItem" {
		t.Fatalf("active approval object closure = %#v, want [WorkItem]", objects)
	}
	candidates := view.RecordObjectCandidateAPINames()
	candidateSet := make(map[string]struct{}, len(candidates))
	for _, candidate := range candidates {
		candidateSet[candidate] = struct{}{}
	}
	if _, ok := candidateSet["WorkItem"]; !ok {
		t.Fatalf("request-bound record object candidates = %#v, missing WorkItem", candidates)
	}
	if _, ok := candidateSet["Unrelated"]; !ok {
		t.Fatalf("request-bound record object candidates = %#v, want projected WorkItem and Unrelated", candidates)
	}
	if _, ok := view.ApprovalDefinition(testApprovalDefinitionA); !ok {
		t.Fatal("selected approval definition is absent")
	}
	if _, ok := view.ActiveFlowVersion(testApprovalFlowName); !ok {
		t.Fatal("approval Flow action dependency is absent from the exact view")
	}
	if _, ok := view.ActiveFlowVersion("NormalizeWorkItem"); ok || len(view.doc.Mutation.TriggerSubscriptions) != 0 {
		t.Fatal("approval view inherited an unrelated record-trigger Flow root")
	}
	if view.AllowsPublicOAuthScope("external.records.read") ||
		view.AllowsPublicOAuthScopeForPack("pack.records.read", "io.example.pack") {
		t.Fatal("approval view leaked OAuth scope authority")
	}
	if len(view.ConnectorInboundAuthorshipBindings("example-connector-pack")) != 0 ||
		len(view.ConnectorInboundFinalizationAuthorities("example-connector-pack")) != 0 {
		t.Fatal("approval view leaked connector execution authority")
	}

	definitions[0].Definition.Label = "mutated"
	again := view.ActiveApprovalDefinitionsForObject("WorkItem")
	if len(again) != 2 || again[0].Definition.Label == "mutated" {
		t.Fatal("caller mutation changed the immutable approval view")
	}

	otherArtifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallB,
		CapabilityID:      approvalv1.ExecuteCapabilityName,
	})
	if err != nil {
		t.Fatalf("ProjectRuntimeMetadataView(other app) error = %v", err)
	}
	other := mustOpenView(t, otherArtifact)
	if objects := other.ActiveApprovalObjectAPINames(); len(objects) != 1 || objects[0] != "WorkItem" {
		t.Fatalf("other app approval object closure = %#v, want [WorkItem]", objects)
	}
	if _, ok := other.ApprovalDefinition(testApprovalDefinitionA); ok {
		t.Fatal("other AppInstallation received an unselected approval definition")
	}
	if _, ok := other.ApprovalDefinition(testApprovalDefinitionB); !ok {
		t.Fatal("other AppInstallation lost its selected approval definition")
	}
	if _, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: "01900000-0000-7000-8000-000000000099",
		CapabilityID:      approvalv1.ExecuteCapabilityName,
	}); err == nil {
		t.Fatal("missing AppInstallation authority did not fail closed")
	}

	otherSnapshot := mustOpenSnapshot(t, mustCompile(t, withRevision(approvalCompileInput(t), testRevision+1)))
	if err := view.VerifySource(otherSnapshot); !errors.Is(err, ErrArtifactCorrupt) {
		t.Fatalf("mixed revision VerifySource() error = %v, want corruption", err)
	}
	if bytes.Contains(artifact.Bytes(), []byte(`"oauthPublicScopes":[{`)) ||
		bytes.Contains(artifact.Bytes(), []byte(`"connectorInboundAuthorship":[{`)) {
		t.Fatalf("approval artifact contains unrelated connector authority: %s", artifact.Bytes())
	}
}

func TestRecordQueryViewProjectsOnlySafeApprovalReferenceIdentities(t *testing.T) {
	input := approvalRecordQueryCompileInput(t)
	snapshot := mustOpenSnapshot(t, mustCompile(t, input))
	artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA, CapabilityID: "runtime.records.query.v1",
	})
	if err != nil {
		t.Fatal(err)
	}
	view := mustOpenView(t, artifact)
	if view.doc.Schema != RuntimeMetadataViewSchemaV20 || view.Profile() != ProjectionRecordQueryV1 {
		t.Fatalf("record-query Approval identity view = schema %q profile %q", view.doc.Schema, view.Profile())
	}
	if len(view.doc.ApprovalDefinitions) != 0 || view.doc.Mutation != nil {
		t.Fatal("record-query view leaked executable Approval authority")
	}
	definitionResolution, err := view.ResolveReferenceIdentities(t.Context(), constants.TableProcessDefinition, []string{testApprovalDefinitionA, "legacy-definition"}, []string{constants.FieldID, constants.FieldName})
	if err != nil {
		t.Fatal(err)
	}
	definition := definitionResolution.Identities[testApprovalDefinitionA]
	if definition.Disposition != referenceidentity.Immutable || definition.Record[constants.FieldName] != "Primary review" {
		t.Fatalf("ProcessDefinition identity = %#v", definition)
	}
	if _, claimed := definitionResolution.Identities["legacy-definition"]; claimed {
		t.Fatal("record-query view claimed an absent legacy ProcessDefinition")
	}
	nodeID := ApprovalNodeStorageID(testApprovalDefinitionA, "Review")
	nodeResolution, err := view.ResolveReferenceIdentities(t.Context(), constants.TableProcessNode, []string{nodeID, "node-legacy", "catalog-approval-node-stale"}, []string{constants.FieldID, constants.FieldName})
	if err != nil {
		t.Fatal(err)
	}
	if got := nodeResolution.Identities[nodeID]; got.Disposition != referenceidentity.Immutable || got.Record[constants.FieldName] != "Review" {
		t.Fatalf("ProcessNode identity = %#v", got)
	}
	if _, claimed := nodeResolution.Identities["node-legacy"]; claimed {
		t.Fatal("record-query view claimed an absent legacy ProcessNode")
	}
	if got := nodeResolution.Identities["catalog-approval-node-stale"].Disposition; got != referenceidentity.OwnedMissing {
		t.Fatalf("stale Catalog node disposition = %v, want owned-missing", got)
	}

	copy := view.doc
	copy.Profile = ProjectionApprovalReadV1
	assertRuntimeDocumentRejected(t, copy)
	copy = view.doc
	copy.Schema = RuntimeMetadataViewSchemaV4
	assertRuntimeDocumentRejected(t, copy)
}

func TestRecordQueryApprovalReferenceIdentitiesRejectMixedCatalogRevision(t *testing.T) {
	input := approvalRecordQueryCompileInput(t)
	input.ApprovalDefinitions[0].CatalogRevision++
	if _, err := CompileCatalogSnapshot(input); err == nil || !strings.Contains(err.Error(), "catalogRevision") {
		t.Fatalf("mixed Approval Catalog revision error = %v", err)
	}
}

func approvalRecordQueryCompileInput(t *testing.T) CompileInput {
	t.Helper()
	input := approvalCompileInput(t)
	stringField := func(object, name string) models.FieldMetadata {
		return models.FieldMetadata{ID: "field-" + strings.ToLower(object) + "-" + strings.ToLower(name), APIName: name, Type: constants.FieldTypeString, IsSystem: true}
	}
	definition := &models.ObjectMetadata{
		ID: "object-processdefinition", APIName: constants.TableProcessDefinition, Label: "Process Definition", PluralLabel: "Process Definitions",
		IsSystem: true, TableType: constants.TableTypeSystemCore, SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{stringField(constants.TableProcessDefinition, constants.FieldID), stringField(constants.TableProcessDefinition, constants.FieldName), stringField(constants.TableProcessDefinition, "DeveloperName")},
	}
	definitionRel := "ProcessDefinition"
	nodeDefinition := models.FieldMetadata{
		ID: "field-processnode-processdefinitionid", APIName: "ProcessDefinitionId", Type: constants.FieldTypeReference, IsSystem: true,
		ReferenceTo: []string{constants.TableProcessDefinition}, RelationshipName: &definitionRel,
	}
	node := &models.ObjectMetadata{
		ID: "object-processnode", APIName: constants.TableProcessNode, Label: "Process Node", PluralLabel: "Process Nodes",
		IsSystem: true, TableType: constants.TableTypeSystemCore, SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{stringField(constants.TableProcessNode, constants.FieldID), stringField(constants.TableProcessNode, constants.FieldName), stringField(constants.TableProcessNode, "DeveloperName"), nodeDefinition},
	}
	input.Objects = append(input.Objects,
		ObjectInput{CatalogRevision: testRevision, Metadata: definition},
		ObjectInput{CatalogRevision: testRevision, Metadata: node},
	)
	for index := range input.Projections {
		input.Projections[index].ObjectAPINames = append(
			input.Projections[index].ObjectAPINames, constants.TableProcessDefinition, constants.TableProcessNode,
		)
	}
	queryObjects := make([]string, 0, len(input.Objects))
	for _, object := range input.Objects {
		queryObjects = append(queryObjects, object.Metadata.APIName)
	}
	input.Projections = append(input.Projections, RuntimeProjectionInput{
		CatalogRevision: testRevision, AppInstallationID: testInstallA,
		CapabilityID: "runtime.records.query.v1", Profile: ProjectionRecordQueryV1,
		ObjectAPINames: queryObjects,
	})
	return input
}

func TestApprovalCompilationIsDeterministicAndDoesNotLeakIntoOtherProfiles(t *testing.T) {
	input := approvalCompileInput(t)
	first := mustCompile(t, input)
	reordered := approvalCompileInput(t)
	reordered.ApprovalDefinitions[0], reordered.ApprovalDefinitions[1] =
		reordered.ApprovalDefinitions[1], reordered.ApprovalDefinitions[0]
	reverseProjections(reordered.Projections)
	for index := range reordered.Projections {
		reverseStrings(reordered.Projections[index].ObjectAPINames)
		reverseStrings(reordered.Projections[index].DefinitionComponentIDs)
	}
	second := mustCompile(t, reordered)
	if first.Digest() != second.Digest() || !bytes.Equal(first.Bytes(), second.Bytes()) {
		t.Fatal("approval input ordering changed canonical Catalog bytes")
	}

	input.Projections = append(input.Projections, RuntimeProjectionInput{
		CatalogRevision:   input.Lineage.CatalogRevision,
		AppInstallationID: testInstallA, CapabilityID: testMutationCapability,
		Profile:        ProjectionRecordMutationV1,
		ObjectAPINames: []string{testOAuthPolicyObject, "Unrelated", "Workspace", "WorkItem"},
	})
	snapshot := mustOpenSnapshot(t, mustCompile(t, input))
	artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA,
		CapabilityID:      testMutationCapability,
	})
	if err != nil {
		t.Fatal(err)
	}
	view := mustOpenView(t, artifact)
	if view.Profile() != ProjectionRecordMutationV1 ||
		view.doc.Schema == RuntimeMetadataViewSchemaV16 || len(view.doc.ApprovalDefinitions) != 0 {
		t.Fatalf("ordinary mutation view acquired Approval authority: profile=%q schema=%q definitions=%d",
			view.Profile(), view.doc.Schema, len(view.doc.ApprovalDefinitions))
	}
	if _, ok := view.ActiveFlowVersion(testApprovalFlowName); ok {
		t.Fatal("ordinary mutation view acquired the Approval-only Flow dependency")
	}
}

func TestApprovalNotificationFlowProjectionUsesNarrowRuntimeProfile(t *testing.T) {
	allowed := runtimeFlowVersionRecord{ID: "approval-notification", Metadata: json.RawMessage(`{
		"status":"Active",
		"start":{},
		"actionCalls":[
			{"name":"Resolve","actionType":"resolveEmailRecipients"},
			{"name":"Email","actionType":"sendEmail"},
			{"name":"Notify","actionType":"sendNotification"}
		]
	}`)}
	if children, err := runtimeApprovalNotificationFlowChildNames(allowed); err != nil || len(children) != 0 {
		t.Fatalf("notification-only Flow closure = %v, %v", children, err)
	}

	for name, metadata := range map[string]string{
		"record mutation": `{"status":"Active","start":{},"recordUpdates":[{"name":"Write","object":"WorkItem"}]}`,
		"subflow":         `{"status":"Active","start":{},"subflows":[{"name":"Child","flowName":"HiddenEffect"}]}`,
		"connector":       `{"status":"Active","start":{},"actionCalls":[{"name":"Push","actionType":"PushToConnector"}]}`,
	} {
		t.Run(name, func(t *testing.T) {
			version := runtimeFlowVersionRecord{ID: name, Metadata: json.RawMessage(metadata)}
			if _, err := runtimeApprovalNotificationFlowChildNames(version); err == nil {
				t.Fatal("unsafe approval notification Flow was accepted")
			}
		})
	}
}

func TestApprovalReadViewCannotCarryExecutionAuthority(t *testing.T) {
	snapshot := mustOpenSnapshot(t, mustCompile(t, approvalCompileInput(t)))
	artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA,
		CapabilityID:      approvalv1.ReadCapabilityName,
	})
	if err != nil {
		t.Fatalf("ProjectRuntimeMetadataView() error = %v", err)
	}
	view := mustOpenView(t, artifact)
	if view.doc.Schema != RuntimeMetadataViewSchemaV16 || view.Profile() != ProjectionApprovalReadV1 {
		t.Fatalf("approval read view = schema %q profile %q", view.doc.Schema, view.Profile())
	}
	if view.doc.Mutation != nil {
		t.Fatal("approval read view contains a mutation closure")
	}
	if view.SupportsMutationClosure() {
		t.Fatal("approval read view acquired mutation authority")
	}
	if _, ok := view.ApprovalDefinition(testApprovalDefinitionA); !ok {
		t.Fatal("approval read view lost its selected immutable definition")
	}
	if _, ok := view.ActiveFlowVersion(testApprovalFlowName); ok {
		t.Fatal("approval read view inherited an executable Flow dependency")
	}
	for _, object := range view.doc.Objects {
		if object.Mutation != nil || object.RequiredWritePermission != "" || object.LockRule != nil {
			t.Fatalf("approval read object %q contains write authority", object.APIName)
		}
		if object.AccessPolicy != nil && (object.AccessPolicy.ObjectCreate != nil ||
			object.AccessPolicy.ObjectEdit != nil || object.AccessPolicy.ObjectDelete != nil ||
			object.AccessPolicy.FieldEdit != nil || object.AccessPolicy.RecordMutation != nil) {
			t.Fatalf("approval read object %q contains write access policy", object.APIName)
		}
	}
}

func TestApprovalPublicationFailsClosedBeforeServing(t *testing.T) {
	t.Run("mixed revision", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.ApprovalDefinitions[0].CatalogRevision++
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("mixed approval revision was accepted")
		}
	})
	t.Run("malformed envelope", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.ApprovalDefinitions[0].Envelope = []byte(`{"schema":"nexuscrm.approval-definition/v1"}`)
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("malformed approval envelope was accepted")
		}
	})
	t.Run("noncanonical object", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.ApprovalDefinitions[0].ObjectAPIName = "workitem"
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("noncanonical approval object identity was accepted")
		}
	})
	t.Run("duplicate definition id", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.ApprovalDefinitions[1].DefinitionID = input.ApprovalDefinitions[0].DefinitionID
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("duplicate approval definition identity was accepted")
		}
	})
	t.Run("unresolved action", func(t *testing.T) {
		input := approvalCompileInput(t)
		definition := approvalDefinitionFixture(1, "")
		definition.FinalApprovalActions = []approvalir.Action{{
			Name: "Unresolved", Kind: approvalir.ActionKindFlow,
			Reference: &approvalir.ActionReference{APIName: "FlowActionReference"},
		}}
		input.ApprovalDefinitions[0].Envelope = approvalEnvelope(t, &definition)
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("unresolved approval action entered a serving Catalog")
		}
	})
	t.Run("missing Flow dependency", func(t *testing.T) {
		input := approvalCompileInput(t)
		definition := approvalDefinitionFixture(1, "AbsentApprovalFlow")
		input.ApprovalDefinitions[0].Envelope = approvalEnvelope(t, &definition)
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("approval definition with an absent Flow dependency was accepted")
		}
	})
	t.Run("duplicate object name", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.ApprovalDefinitions[1].DeveloperName = input.ApprovalDefinitions[0].DeveloperName
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("duplicate approval object/name identity was accepted")
		}
	})
	t.Run("non approval projection", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.Projections[0].Profile = ProjectionRecordMutationV1
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("non-approval profile selected approval authority")
		}
	})
	t.Run("wrong selected definition kind", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.Projections[0].DefinitionComponentIDs = []string{"definition.unrelated"}
		if _, err := CompileCatalogSnapshot(input); err == nil {
			t.Fatal("approval profile selected a non-approval definition")
		}
	})
	t.Run("execution without selected definition", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.Projections[0].DefinitionComponentIDs = nil
		snapshot := mustOpenSnapshot(t, mustCompile(t, input))
		artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
			AppInstallationID: testInstallA,
			CapabilityID:      approvalv1.ExecuteCapabilityName,
		})
		if err != nil {
			t.Fatalf("ProjectRuntimeMetadataView() error = %v", err)
		}
		view := mustOpenView(t, artifact)
		if view.Profile() != ProjectionApprovalExecutionV1 ||
			len(view.doc.ApprovalDefinitions) != 0 || len(view.doc.Definitions) != 0 ||
			view.doc.Mutation != nil || view.SupportsMutationClosure() {
			t.Fatal("empty approval execution projection acquired definition or mutation authority")
		}
		for _, object := range view.doc.Objects {
			if object.RequiredWritePermission != "" || object.LockRule != nil || object.Mutation != nil {
				t.Fatalf("empty approval execution object %q acquired write authority", object.APIName)
			}
			for _, field := range object.Fields {
				if field.Mutation != nil {
					t.Fatalf("empty approval execution field %q.%s acquired write authority", object.APIName, field.APIName)
				}
			}
		}
		tampered := view.doc
		tampered.Mutation = emptyMutationDefinitionDocument()
		assertRuntimeDocumentRejected(t, tampered)
	})
	t.Run("read without selected definition", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.Projections[2].DefinitionComponentIDs = nil
		snapshot := mustOpenSnapshot(t, mustCompile(t, input))
		artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
			AppInstallationID: testInstallA,
			CapabilityID:      approvalv1.ReadCapabilityName,
		})
		if err != nil {
			t.Fatalf("ProjectRuntimeMetadataView() error = %v", err)
		}
		view := mustOpenView(t, artifact)
		if len(view.doc.ApprovalDefinitions) != 0 || view.doc.Mutation != nil {
			t.Fatal("empty approval read projection acquired approval or mutation authority")
		}
	})
	t.Run("selected definition object outside read closure", func(t *testing.T) {
		input := approvalCompileInput(t)
		input.Projections[2].ObjectAPINames = []string{testOAuthPolicyObject, "Unrelated", "Workspace"}
		if _, err := CompileCatalogSnapshot(input); err == nil ||
			!strings.Contains(err.Error(), "outside the projection closure") {
			t.Fatalf("approval read outside object closure error = %v", err)
		}
	})
}

func TestApprovalCatalogArtifactRejectsConflictingLineageAndProjectionClosure(t *testing.T) {
	artifact := mustCompile(t, approvalCompileInput(t))
	var source catalogDocument
	if err := decodeStrict(artifact.Bytes(), &source); err != nil {
		t.Fatal(err)
	}

	for _, test := range []struct {
		name   string
		mutate func(*definitionRecord)
	}{
		{"definition version", func(lineage *definitionRecord) { lineage.DefinitionVersion = "tampered-version" }},
		{"content digest", func(lineage *definitionRecord) {
			lineage.ContentDigest = Digest("sha256:" + string(bytes.Repeat([]byte{'f'}, 64)))
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			copy := source
			copy.Definitions = append([]definitionRecord{}, source.Definitions...)
			test.mutate(&copy.Definitions[approvalLineageIndex(t, copy.Definitions, testApprovalDefinitionA)])
			assertCatalogDocumentRejected(t, copy)
		})
	}

	t.Run("definition object outside projection closure", func(t *testing.T) {
		copy := source
		copy.Projections = append([]projectionRecord{}, source.Projections...)
		for index := range copy.Projections {
			if copy.Projections[index].Profile != ProjectionApprovalReadV1 ||
				copy.Projections[index].AppInstallationID != testInstallA {
				continue
			}
			objects := make([]string, 0, len(copy.Projections[index].ObjectAPINames))
			for _, objectAPIName := range copy.Projections[index].ObjectAPINames {
				if objectAPIName != "WorkItem" {
					objects = append(objects, objectAPIName)
				}
			}
			copy.Projections[index].ObjectAPINames = objects
			data, err := marshalCanonical(copy)
			if err != nil {
				t.Fatal(err)
			}
			if _, err := OpenCatalogSnapshot(makeArtifact(ArtifactCatalogSnapshot, data)); err == nil ||
				!strings.Contains(err.Error(), "outside its object closure") {
				t.Fatalf("tampered approval projection object closure error = %v", err)
			}
			return
		}
		t.Fatal("approval read projection fixture is absent")
	})

	for _, test := range []struct {
		name   string
		mutate func(*approvalDefinitionRecord)
		want   string
	}{
		{"definition identity differs from lineage", func(definition *approvalDefinitionRecord) {
			definition.DefinitionID = strings.ToUpper(definition.DefinitionID)
		}, "conflicts with its definition lineage"},
		{"noncanonical object identity", func(definition *approvalDefinitionRecord) {
			definition.ObjectAPIName = strings.ToLower(definition.ObjectAPIName)
		}, "absent or noncanonical object"},
	} {
		t.Run(test.name, func(t *testing.T) {
			copy := source
			copy.ApprovalDefinitions = append([]approvalDefinitionRecord{}, source.ApprovalDefinitions...)
			test.mutate(&copy.ApprovalDefinitions[0])
			if err := openCatalogDocumentError(t, copy); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("tampered approval identity error = %v, want %q", err, test.want)
			}
		})
	}

	t.Run("missing approval-only Flow closure", func(t *testing.T) {
		copy := source
		mutation := cloneMutationWithoutApprovalOnlyFlow(t, source.Mutation)
		copy.Mutation = mutation
		if err := openCatalogDocumentError(t, copy); err == nil ||
			!strings.Contains(err.Error(), "incomplete approval execution closure") {
			t.Fatalf("missing Catalog approval Flow closure error = %v", err)
		}
	})
}

func TestApprovalRuntimeArtifactRejectsMixedAuthority(t *testing.T) {
	snapshot := mustOpenSnapshot(t, mustCompile(t, approvalCompileInput(t)))
	artifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA,
		CapabilityID:      approvalv1.ExecuteCapabilityName,
	})
	if err != nil {
		t.Fatal(err)
	}
	var doc runtimeViewDocument
	if err := decodeStrict(artifact.Bytes(), &doc); err != nil {
		t.Fatal(err)
	}
	readArtifact, err := ProjectRuntimeMetadataView(snapshot, ProjectionKey{
		AppInstallationID: testInstallA,
		CapabilityID:      approvalv1.ReadCapabilityName,
	})
	if err != nil {
		t.Fatal(err)
	}
	var readDoc runtimeViewDocument
	if err := decodeStrict(readArtifact.Bytes(), &readDoc); err != nil {
		t.Fatal(err)
	}

	t.Run("wrong schema", func(t *testing.T) {
		copy := doc
		copy.Schema = RuntimeMetadataViewSchemaV15
		assertRuntimeDocumentRejected(t, copy)
	})
	t.Run("wrong profile", func(t *testing.T) {
		copy := doc
		copy.Profile = ProjectionRecordMutationV1
		assertRuntimeDocumentRejected(t, copy)
	})
	t.Run("OAuth authority", func(t *testing.T) {
		copy := doc
		mutation := *doc.Mutation
		mutation.OAuthPublicScopes = append([]OAuthPublicScopeRule{}, doc.Mutation.OAuthPublicScopes...)
		mutation.OAuthPublicScopes = append(mutation.OAuthPublicScopes, OAuthPublicScopeRule{
			ObjectAPIName: "CredentialClient", Name: "external.records.read",
		})
		copy.Mutation = &mutation
		assertRuntimeDocumentRejected(t, copy)
	})
	t.Run("definition content", func(t *testing.T) {
		copy := doc
		copy.ApprovalDefinitions = append([]approvalDefinitionRecord{}, doc.ApprovalDefinitions...)
		copy.ApprovalDefinitions[0].Definition.Label = "tampered label"
		assertRuntimeDocumentRejected(t, copy)
	})
	t.Run("execution without definition authority", func(t *testing.T) {
		copy := doc
		copy.ApprovalDefinitions = []approvalDefinitionRecord{}
		assertRuntimeDocumentRejected(t, copy)
	})
	for _, test := range []struct {
		name   string
		mutate func(*definitionRecord)
	}{
		{"definition lineage version", func(lineage *definitionRecord) { lineage.DefinitionVersion = "tampered-version" }},
		{"definition lineage digest", func(lineage *definitionRecord) {
			lineage.ContentDigest = Digest("sha256:" + string(bytes.Repeat([]byte{'e'}, 64)))
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			copy := doc
			copy.Definitions = append([]definitionRecord{}, doc.Definitions...)
			test.mutate(&copy.Definitions[approvalLineageIndex(t, copy.Definitions, testApprovalDefinitionA)])
			assertRuntimeDocumentRejected(t, copy)
		})
	}
	for _, test := range []struct {
		name   string
		mutate func(*approvalDefinitionRecord)
		want   string
	}{
		{"definition identity differs from lineage", func(definition *approvalDefinitionRecord) {
			definition.DefinitionID = strings.ToUpper(definition.DefinitionID)
		}, "conflicts with its definition lineage"},
		{"noncanonical object identity", func(definition *approvalDefinitionRecord) {
			definition.ObjectAPIName = strings.ToLower(definition.ObjectAPIName)
		}, "absent or noncanonical object"},
	} {
		t.Run(test.name, func(t *testing.T) {
			copy := doc
			copy.ApprovalDefinitions = append([]approvalDefinitionRecord{}, doc.ApprovalDefinitions...)
			test.mutate(&copy.ApprovalDefinitions[0])
			if err := openRuntimeDocumentError(t, copy); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("tampered Runtime approval identity error = %v, want %q", err, test.want)
			}
		})
	}
	t.Run("definition lineage outside approval authority", func(t *testing.T) {
		copy := doc
		copy.Definitions = append([]definitionRecord{}, doc.Definitions...)
		copy.Definitions = append(copy.Definitions, definitionRecord{
			ComponentID: "zzzz.unrelated", DefinitionVersion: "1",
			ContentDigest: Digest("sha256:" + string(bytes.Repeat([]byte{'d'}, 64))),
		})
		if err := openRuntimeDocumentError(t, copy); err == nil ||
			!strings.Contains(err.Error(), "outside approval authority") {
			t.Fatalf("extra Runtime definition lineage error = %v", err)
		}
	})
	t.Run("missing approval-only Flow closure", func(t *testing.T) {
		copy := doc
		copy.Mutation = cloneMutationWithoutApprovalOnlyFlow(t, doc.Mutation)
		if err := openRuntimeDocumentError(t, copy); err == nil ||
			!strings.Contains(err.Error(), "incomplete execution closure") {
			t.Fatalf("missing Runtime approval Flow closure error = %v", err)
		}
	})
	t.Run("unselected Flow authority", func(t *testing.T) {
		copy := doc
		mutation := *doc.Mutation
		mutation.FlowDefinitions = append([]runtimeFlowDefinitionRecord{}, doc.Mutation.FlowDefinitions...)
		mutation.FlowVersions = append([]runtimeFlowVersionRecord{}, doc.Mutation.FlowVersions...)
		definition, version := approvalOnlyFlowRecords(t, doc.Mutation)
		definition.ID = "flow-definition-unselected"
		definition.DeveloperName = "UnselectedApprovalFlow"
		definition.ActiveVersionID = "flow-version-unselected"
		version.ID = definition.ActiveVersionID
		version.DefinitionID = definition.ID
		mutation.FlowDefinitions = append(mutation.FlowDefinitions, definition)
		mutation.FlowVersions = append(mutation.FlowVersions, version)
		sortMutationDefinitions(&mutation)
		copy.Mutation = &mutation
		if err := openRuntimeDocumentError(t, copy); err == nil ||
			!strings.Contains(err.Error(), "contains unselected authority") {
			t.Fatalf("extra Runtime approval Flow authority error = %v", err)
		}
	})
	t.Run("read profile write permission", func(t *testing.T) {
		copy := readDoc
		copy.Objects = append([]runtimeObjectRecord{}, readDoc.Objects...)
		copy.Objects[0].RequiredWritePermission = "must-not-enter-approval-read"
		assertRuntimeDocumentRejected(t, copy)
	})
	t.Run("read profile lock rule", func(t *testing.T) {
		copy := readDoc
		copy.Objects = append([]runtimeObjectRecord{}, readDoc.Objects...)
		lockRule := "TRUE"
		copy.Objects[0].LockRule = &lockRule
		assertRuntimeDocumentRejected(t, copy)
	})
}

func approvalCompileInput(t *testing.T) CompileInput {
	t.Helper()
	input := mutationCompileInput()
	input.MutationDefinitions = append(input.MutationDefinitions,
		MutationDefinitionInput{CatalogRevision: testRevision, FlowDefinition: &models.FlowDefinition{
			Id: "flow-definition-approval-only", DeveloperName: testApprovalFlowName,
			ProcessType: "AutoLaunchedFlow", ActiveVersionId: "flow-version-approval-only",
		}},
		MutationDefinitionInput{CatalogRevision: testRevision, FlowVersion: &models.FlowVersion{
			Id: "flow-version-approval-only", DefinitionId: "flow-definition-approval-only", VersionNumber: 1,
			Status: models.FlowStatusActive, ProcessType: "AutoLaunchedFlow", Source: "standard",
			MetadataRaw: []byte(`{"status":"Active","start":{}}`),
		}},
	)
	primary := approvalDefinitionFixture(2, testApprovalFlowName)
	// Keep the display label deliberately distinct: ProcessNode.Name must
	// preserve the authored developer identity used by the legacy public API.
	primary.Steps[0].Label = "Human review label"
	secondary := approvalDefinitionFixture(1, "")
	secondary.Label = "Secondary review"
	input.ApprovalDefinitions = []ApprovalDefinitionInput{
		{CatalogRevision: testRevision, DefinitionID: testApprovalDefinitionA, ObjectAPIName: "WorkItem", DeveloperName: "PrimaryReview", Envelope: approvalEnvelope(t, &primary)},
		{CatalogRevision: testRevision, DefinitionID: testApprovalDefinitionB, ObjectAPIName: "WorkItem", DeveloperName: "SecondaryReview", Envelope: approvalEnvelope(t, &secondary)},
	}
	objects := []string{testOAuthPolicyObject, "Unrelated", "Workspace", "WorkItem"}
	input.Projections = []RuntimeProjectionInput{
		{CatalogRevision: testRevision, AppInstallationID: testInstallA, CapabilityID: approvalv1.ExecuteCapabilityName, Profile: ProjectionApprovalExecutionV1, ObjectAPINames: objects, DefinitionComponentIDs: []string{testApprovalDefinitionA, testApprovalDefinitionB}},
		{CatalogRevision: testRevision, AppInstallationID: testInstallB, CapabilityID: approvalv1.ExecuteCapabilityName, Profile: ProjectionApprovalExecutionV1, ObjectAPINames: objects, DefinitionComponentIDs: []string{testApprovalDefinitionB}},
		{CatalogRevision: testRevision, AppInstallationID: testInstallA, CapabilityID: approvalv1.ReadCapabilityName, Profile: ProjectionApprovalReadV1, ObjectAPINames: objects, DefinitionComponentIDs: []string{testApprovalDefinitionA, testApprovalDefinitionB}},
		{CatalogRevision: testRevision, AppInstallationID: testInstallB, CapabilityID: approvalv1.ReadCapabilityName, Profile: ProjectionApprovalReadV1, ObjectAPINames: objects, DefinitionComponentIDs: []string{testApprovalDefinitionB}},
	}
	return input
}

func approvalDefinitionFixture(order int, flowName string) approvalir.Definition {
	definition := approvalir.Definition{
		Label: "Primary review", Active: true, ProcessOrder: &order,
		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{},
	}
	if flowName != "" {
		definition.FinalApprovalActions = []approvalir.Action{{
			Name: "Run follow-up", Kind: approvalir.ActionKindFlow,
			Flow: &approvalir.FlowAction{APIName: flowName},
		}}
	}
	return definition
}

func approvalEnvelope(t *testing.T, definition *approvalir.Definition) []byte {
	t.Helper()
	value, err := approvalir.EncodeDefinitionEnvelope([]byte("product-owned approval source"), definition, nil)
	if err != nil {
		t.Fatalf("EncodeDefinitionEnvelope() error = %v", err)
	}
	return value
}

func assertRuntimeDocumentRejected(t *testing.T, doc runtimeViewDocument) {
	t.Helper()
	if err := openRuntimeDocumentError(t, doc); err == nil {
		t.Fatal("tampered Runtime approval view was accepted")
	}
}

func openRuntimeDocumentError(t *testing.T, doc runtimeViewDocument) error {
	t.Helper()
	data, err := marshalCanonical(doc)
	if err != nil {
		t.Fatal(err)
	}
	artifact := makeArtifact(ArtifactRuntimeMetadataView, data)
	_, err = OpenRuntimeMetadataView(artifact)
	return err
}

func assertCatalogDocumentRejected(t *testing.T, doc catalogDocument) {
	t.Helper()
	if err := openCatalogDocumentError(t, doc); err == nil {
		t.Fatal("tampered Catalog approval artifact was accepted")
	}
}

func openCatalogDocumentError(t *testing.T, doc catalogDocument) error {
	t.Helper()
	data, err := marshalCanonical(doc)
	if err != nil {
		t.Fatal(err)
	}
	_, err = OpenCatalogSnapshot(makeArtifact(ArtifactCatalogSnapshot, data))
	return err
}

func approvalLineageIndex(t *testing.T, definitions []definitionRecord, definitionID string) int {
	t.Helper()
	for index := range definitions {
		if definitions[index].ComponentID == definitionID {
			return index
		}
	}
	t.Fatalf("definition lineage %q is absent", definitionID)
	return -1
}

func approvalOnlyFlowRecords(
	t *testing.T,
	doc *mutationDefinitionDocument,
) (runtimeFlowDefinitionRecord, runtimeFlowVersionRecord) {
	t.Helper()
	if doc == nil {
		t.Fatal("mutation definition closure is absent")
	}
	for _, definition := range doc.FlowDefinitions {
		if definition.DeveloperName != testApprovalFlowName {
			continue
		}
		for _, version := range doc.FlowVersions {
			if version.ID == definition.ActiveVersionID && version.DefinitionID == definition.ID {
				return definition, version
			}
		}
	}
	t.Fatalf("approval-only Flow %q is absent", testApprovalFlowName)
	return runtimeFlowDefinitionRecord{}, runtimeFlowVersionRecord{}
}

func cloneMutationWithoutApprovalOnlyFlow(
	t *testing.T,
	source *mutationDefinitionDocument,
) *mutationDefinitionDocument {
	t.Helper()
	definition, _ := approvalOnlyFlowRecords(t, source)
	copy := *source
	copy.FlowDefinitions = make([]runtimeFlowDefinitionRecord, 0, len(source.FlowDefinitions)-1)
	for _, candidate := range source.FlowDefinitions {
		if candidate.ID != definition.ID {
			copy.FlowDefinitions = append(copy.FlowDefinitions, candidate)
		}
	}
	copy.FlowVersions = make([]runtimeFlowVersionRecord, 0, len(source.FlowVersions)-1)
	for _, candidate := range source.FlowVersions {
		if candidate.DefinitionID != definition.ID {
			copy.FlowVersions = append(copy.FlowVersions, candidate)
		}
	}
	return &copy
}
