package catalogreconcile

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

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	consumerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	pagelayoutresolutionv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
	permissionauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/relatedcountsv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
)

func TestParseConfigRejectsAmbiguityAndBuildsExplicitTrust(t *testing.T) {
	raw := fixtureConfigJSON()
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatalf("parse config: %v", err)
	}
	if config.ReleaseImport == nil {
		t.Fatal("release import config is missing")
	}
	trust, err := config.ReleaseImport.PublisherTrust()
	if err != nil {
		t.Fatal(err)
	}
	if trust.PublisherID != "io.nexuscrm" || !trust.Active || len(trust.Keys) != 1 || string(trust.Keys[0].PublicKey) != "public-key" {
		t.Fatalf("publisher trust = %#v", trust)
	}
	if config.ExpectedCatalogRevision == nil || *config.ExpectedCatalogRevision != 0 ||
		config.ExpectedSecurityEpoch == nil || *config.ExpectedSecurityEpoch != 0 {
		t.Fatalf("expected fences = catalog %v security %v", config.ExpectedCatalogRevision, config.ExpectedSecurityEpoch)
	}
	ambiguous := strings.Replace(raw, `"orgId":"org-test"`, `"orgId":"reviewed","orgId":"org-test"`, 1)
	if _, err := ParseConfig([]byte(ambiguous)); err == nil || !strings.Contains(err.Error(), "duplicate") {
		t.Fatalf("duplicate config error = %v", err)
	}
	for _, field := range []string{`"expectedCatalogRevision":0,`, `"expectedSecurityEpoch":0,`} {
		missing := strings.Replace(raw, field, "", 1)
		if _, err := ParseConfig([]byte(missing)); err == nil || !strings.Contains(err.Error(), "expected Catalog revision") {
			t.Fatalf("missing %s error = %v", field, err)
		}
	}
}

func TestParseConfigAllowsRegisteredReleaseWithoutImportMaterial(t *testing.T) {
	raw := fixtureConfigJSON()
	start := strings.Index(raw, `"releaseImport":`)
	end := strings.Index(raw[start:], "\n\"artifactStoreRoot\"")
	if start < 0 || end < 0 {
		t.Fatal("fixture releaseImport block not found")
	}
	raw = raw[:start] + raw[start+end+1:]
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatalf("parse registered-release-only config: %v", err)
	}
	if config.ReleaseImport != nil {
		t.Fatalf("release import = %#v", config.ReleaseImport)
	}
}

func TestParseConfigRejectsPartialReleaseImportMaterial(t *testing.T) {
	raw := strings.Replace(fixtureConfigJSON(), `"attestationPath":"/artifacts/attestation.json",`, "", 1)
	if _, err := ParseConfig([]byte(raw)); err == nil || !strings.Contains(err.Error(), "packagePath and attestationPath") {
		t.Fatalf("partial release import error = %v", err)
	}
}

func TestParseConfigScopesPhaseOnePublicationToV4AndCanonicalCommand(t *testing.T) {
	publication := `"phaseOnePublication":{"commandId":"01900000-0000-7000-8000-000000000099"},`
	v1 := strings.Replace(fixtureConfigJSON(), `"orgId":"org-test",`, `"orgId":"org-test",`+publication, 1)
	if _, err := ParseConfig([]byte(v1)); err == nil || !strings.Contains(err.Error(), "requires config schema v4") {
		t.Fatalf("v1 phase-one publication error = %v", err)
	}

	v4 := strings.Replace(v1, ConfigSchemaV1, ConfigSchemaV4, 1)
	v4 = strings.Replace(v4,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`",`,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`, 1)
	v4 = strings.Replace(v4, `"workloadId":"workload-test",`,
		`"workloadId":"workload-test","authorizedWorkloadIds":["workload-test"],`, 1)
	v4 = strings.Replace(v4,
		`"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",`,
		`"capabilities":[{"appCapabilityId":"app.query.v1","handlerRef":"io.nexuscrm.sfcrm.handler.query.v1","runtimeCapabilityId":"runtime.records.query.v1"},{"appCapabilityId":"app.record-access.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1","runtimeCapabilityId":"runtime.records.evaluate-access.v1"}],`, 1)
	config, err := ParseConfig([]byte(v4))
	if err != nil {
		t.Fatal(err)
	}
	if config.PhaseOnePublication == nil ||
		config.PhaseOnePublication.CommandID != "01900000-0000-7000-8000-000000000099" {
		t.Fatalf("phase-one publication = %#v", config.PhaseOnePublication)
	}
	invalid := strings.Replace(v4, "01900000-0000-7000-8000-000000000099", "01900000-0000-6000-8000-000000000099", 1)
	if _, err := ParseConfig([]byte(invalid)); err == nil || !strings.Contains(err.Error(), "lowercase UUIDv7") {
		t.Fatalf("invalid phase-one command error = %v", err)
	}
}

func TestParseConfigV2RequiresMetadataSourceDigest(t *testing.T) {
	raw := strings.Replace(fixtureConfigJSON(), ConfigSchemaV1, ConfigSchemaV2, 1)
	if _, err := ParseConfig([]byte(raw)); err == nil || !strings.Contains(err.Error(), "metadata source digest") {
		t.Fatalf("missing v2 metadata source digest error = %v", err)
	}
	raw = strings.Replace(raw, `"expectedAppReleaseDigest":"`+digestOf('a')+`",`, `"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`, 1)
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatal(err)
	}
	request, err := config.BuildRequest(fixtureRegisteredRelease())
	if err != nil {
		t.Fatal(err)
	}
	if request.MetadataSourceDigest != catalogsnapshot.Digest(digestOf('8')) {
		t.Fatalf("metadata source digest = %q", request.MetadataSourceDigest)
	}
}

func TestBuildRequestPreservesLegacySingularReconcileRepresentation(t *testing.T) {
	tests := []struct {
		name         string
		configRaw    func() string
		outputSchema string
	}{
		{name: "v1", configRaw: fixtureConfigJSON, outputSchema: OutputSchemaV1},
		{
			name: "v2",
			configRaw: func() string {
				raw := strings.Replace(fixtureConfigJSON(), ConfigSchemaV1, ConfigSchemaV2, 1)
				return strings.Replace(raw, `"expectedAppReleaseDigest":"`+digestOf('a')+`",`, `"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`, 1)
			},
			outputSchema: OutputSchemaV2,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			config, err := ParseConfig([]byte(test.configRaw()))
			if err != nil {
				t.Fatal(err)
			}
			request, err := config.BuildRequest(fixtureRegisteredRelease())
			if err != nil {
				t.Fatal(err)
			}
			if len(request.Capabilities) != 0 || request.AppCapabilityID == "" || request.HandlerRef == "" || request.RuntimeCapabilityID == "" {
				t.Fatalf("legacy request representation = %#v", request)
			}
			closed, err := bindContinuationIntent(request)
			if err != nil {
				t.Fatalf("close BuildRequest result: %v", err)
			}
			if closed.profile.outputSchema != test.outputSchema {
				t.Fatalf("closed output schema = %q, want %q", closed.profile.outputSchema, test.outputSchema)
			}
		})
	}
}

func TestParseConfigKeepsLegacySchemasRecordAccessOnly(t *testing.T) {
	for _, schema := range []string{ConfigSchemaV1, ConfigSchemaV2} {
		t.Run(schema, func(t *testing.T) {
			raw := fixtureConfigJSON()
			if schema == ConfigSchemaV2 {
				raw = strings.Replace(raw, ConfigSchemaV1, ConfigSchemaV2, 1)
				raw = strings.Replace(raw, `"expectedAppReleaseDigest":"`+digestOf('a')+`",`, `"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`, 1)
			}
			raw = strings.Replace(raw, `"runtimeCapabilityId":"`+recordaccessv1.CapabilityName+`"`, `"runtimeCapabilityId":"`+queryv1.CapabilityName+`"`, 1)
			if _, err := ParseConfig([]byte(raw)); err == nil || !strings.Contains(err.Error(), "does not support") {
				t.Fatalf("legacy query capability error = %v", err)
			}
		})
	}
}

func TestBuildRequestBindsLifecycleAndCompleteD1ReleaseShape(t *testing.T) {
	config, err := ParseConfig([]byte(fixtureConfigJSON()))
	if err != nil {
		t.Fatal(err)
	}
	release := fixtureRegisteredRelease()
	request, err := config.BuildRequest(release)
	if err != nil {
		t.Fatal(err)
	}
	if request.Operation.Kind != catalogapp.OperationPrepare || request.Operation.AppInstallationID != "" ||
		request.Frontend == nil || request.Frontend.ReleaseDigest != digestOf('c') {
		t.Fatalf("request = %#v", request)
	}

	upgrade := strings.Replace(fixtureConfigJSON(), `"operationKind":"PREPARE"`, `"operationKind":"UPGRADE"`, 1)
	upgrade = strings.Replace(upgrade, `"appInstallationId":""`, `"appInstallationId":"01900000-0000-7000-8000-000000000001"`, 1)
	config, err = ParseConfig([]byte(upgrade))
	if err != nil {
		t.Fatal(err)
	}
	request, err = config.BuildRequest(release)
	if err != nil || request.Operation.Kind != catalogapp.OperationUpgrade || request.Operation.AppInstallationID != "01900000-0000-7000-8000-000000000001" {
		t.Fatalf("upgrade request = %#v, %v", request, err)
	}

	missingFrontend := config
	missingFrontend.Frontend = nil
	if _, err := missingFrontend.BuildRequest(release); err == nil || !strings.Contains(err.Error(), "requires a frontend") {
		t.Fatalf("missing frontend error = %v", err)
	}
	extraCapability := release
	extraCapability.Package.Manifest.ProvidedCapabilities = append(extraCapability.Package.Manifest.ProvidedCapabilities,
		appauthority.ProvidedCapability{CapabilityID: "io.nexuscrm.sfcrm.extra.v1", Kind: appauthority.CapabilityKindMetadata})
	if _, err := config.BuildRequest(extraCapability); err == nil || !strings.Contains(err.Error(), "cover every provided") {
		t.Fatalf("multi-capability error = %v", err)
	}
	wrongRuntime := config
	wrongRuntime.RuntimeCapabilityID = "runtime.records.other.v1"
	if _, err := wrongRuntime.BuildRequest(release); err == nil || !strings.Contains(err.Error(), "does not support") {
		t.Fatalf("unsupported Runtime capability error = %v", err)
	}
	extraRuntimeGrant := fixtureRegisteredRelease()
	extraRuntimeGrant.Package.Manifest.RequiredEngineCapabilities = append(
		extraRuntimeGrant.Package.Manifest.RequiredEngineCapabilities,
		"runtime.records.read-selected-root.v1",
	)
	if _, err := config.BuildRequest(extraRuntimeGrant); err == nil || !strings.Contains(err.Error(), "does not match") {
		t.Fatalf("multi-grant Runtime capability error = %v", err)
	}
	missingRuntimeGrant := fixtureRegisteredRelease()
	missingRuntimeGrant.Package.Manifest.RequiredEngineCapabilities = nil
	if _, err := config.BuildRequest(missingRuntimeGrant); err == nil || !strings.Contains(err.Error(), "does not match") {
		t.Fatalf("missing Runtime capability grant error = %v", err)
	}
}

func TestBuildRequestV3RequiresCompleteSynchronousReadCapabilityFamily(t *testing.T) {
	raw := strings.Replace(fixtureConfigJSON(), ConfigSchemaV1, ConfigSchemaV3, 1)
	raw = strings.Replace(raw,
		`"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",`,
		`"capabilities":[{"appCapabilityId":"io.nexuscrm.sfcrm.ui.record-access.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1","runtimeCapabilityId":"runtime.records.evaluate-access.v1"},{"appCapabilityId":"io.nexuscrm.sfcrm.query.v1","handlerRef":"io.nexuscrm.sfcrm.handler.query.v1","runtimeCapabilityId":"runtime.records.query.v1"}],`, 1)
	raw = strings.Replace(raw, `"expectedAppReleaseDigest":"`+digestOf('a')+`",`, `"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`, 1)
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatal(err)
	}
	release := fixtureRegisteredRelease()
	release.Package.Manifest.RequiredEngineCapabilities = []string{"runtime.records.evaluate-access.v1", "runtime.records.query.v1"}
	release.Package.Manifest.ProvidedCapabilities = []appauthority.ProvidedCapability{
		{CapabilityID: "io.nexuscrm.sfcrm.ui.record-access.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.ui.record-access.v1", RequiredRuntimeCapabilities: []string{"runtime.records.evaluate-access.v1"}},
		{CapabilityID: "io.nexuscrm.sfcrm.query.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.query.v1", RequiredRuntimeCapabilities: []string{"runtime.records.query.v1"}},
	}
	request, err := config.BuildRequest(release)
	if err != nil {
		t.Fatal(err)
	}
	if len(request.Capabilities) != 2 || len(request.Operation.GrantedCapabilities) != 2 || request.AppCapabilityID != "" {
		t.Fatalf("v3 request = %#v", request)
	}
	if request.Capabilities[0].RuntimeCapabilityID != "runtime.records.query.v1" {
		t.Fatalf("App capability order = %#v, want query selection first", request.Capabilities)
	}
	if !slices.Equal(request.Operation.GrantedCapabilities, release.Package.Manifest.RequiredEngineCapabilities) {
		t.Fatalf("Runtime grants = %#v, want verified release order %#v",
			request.Operation.GrantedCapabilities, release.Package.Manifest.RequiredEngineCapabilities)
	}
	closed, err := bindContinuationIntent(request)
	if err != nil {
		t.Fatalf("close v3 BuildRequest result: %v", err)
	}
	if closed.profile.outputSchema != OutputSchemaV3 {
		t.Fatalf("closed v3 output schema = %q", closed.profile.outputSchema)
	}
	partial := config
	partial.Capabilities = partial.Capabilities[:1]
	partialRaw, err := json.Marshal(partial)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ParseConfig(partialRaw); err == nil || !strings.Contains(err.Error(), "requires at least two") {
		t.Fatalf("parse partial v3 capability family error = %v", err)
	}
	if _, err := partial.BuildRequest(release); err == nil || !strings.Contains(err.Error(), "requires at least two") {
		t.Fatalf("partial capability family error = %v", err)
	}
	singleRelease := release
	singleRelease.Package.Manifest.RequiredEngineCapabilities = []string{"runtime.records.evaluate-access.v1"}
	singleRelease.Package.Manifest.ProvidedCapabilities = singleRelease.Package.Manifest.ProvidedCapabilities[:1]
	if _, err := partial.BuildRequest(singleRelease); err == nil || !strings.Contains(err.Error(), "requires at least two") {
		t.Fatalf("v3 single-capability release error = %v", err)
	}
}

func TestBuildRequestV3AcceptsCompleteVerifiedCapabilitySet(t *testing.T) {
	raw := strings.Replace(fixtureConfigJSON(), ConfigSchemaV1, ConfigSchemaV3, 1)
	raw = strings.Replace(raw,
		`"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",`,
		`"capabilities":[{"appCapabilityId":"io.nexuscrm.sfcrm.ui.record-access.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1","runtimeCapabilityId":"runtime.records.evaluate-access.v1"},{"appCapabilityId":"io.nexuscrm.sfcrm.query.v1","handlerRef":"io.nexuscrm.sfcrm.handler.query.v1","runtimeCapabilityId":"runtime.records.query.v1"},{"appCapabilityId":"io.nexuscrm.sfcrm.recycle-bin-read.v1","handlerRef":"io.nexuscrm.sfcrm.handler.recycle-bin-read.v1","runtimeCapabilityId":"`+recyclebinv1.CapabilityName+`"},{"appCapabilityId":"io.nexuscrm.sfcrm.related-list-counts.v1","handlerRef":"io.nexuscrm.sfcrm.handler.related-list-counts.v1","runtimeCapabilityId":"`+relatedcountsv1.CapabilityName+`"},{"appCapabilityId":"io.nexuscrm.sfcrm.record-mutation.v1","handlerRef":"io.nexuscrm.sfcrm.handler.record-mutation.v1","runtimeCapabilityId":"`+recordmutationv1.CapabilityName+`"}],`, 1)
	raw = strings.Replace(raw, `"expectedAppReleaseDigest":"`+digestOf('a')+`",`, `"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`, 1)
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatal(err)
	}
	release := fixtureRegisteredRelease()
	release.Package.Manifest.RequiredEngineCapabilities = []string{
		recordaccessv1.CapabilityName, recyclebinv1.CapabilityName, recordmutationv1.CapabilityName,
		queryv1.CapabilityName, relatedcountsv1.CapabilityName,
	}
	release.Package.Manifest.ProvidedCapabilities = []appauthority.ProvidedCapability{
		{CapabilityID: "io.nexuscrm.sfcrm.ui.record-access.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.ui.record-access.v1", RequiredRuntimeCapabilities: []string{recordaccessv1.CapabilityName}},
		{CapabilityID: "io.nexuscrm.sfcrm.query.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.query.v1", RequiredRuntimeCapabilities: []string{queryv1.CapabilityName}},
		{CapabilityID: "io.nexuscrm.sfcrm.recycle-bin-read.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.recycle-bin-read.v1", RequiredRuntimeCapabilities: []string{recyclebinv1.CapabilityName}},
		{CapabilityID: "io.nexuscrm.sfcrm.related-list-counts.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.related-list-counts.v1", RequiredRuntimeCapabilities: []string{relatedcountsv1.CapabilityName}},
		{CapabilityID: "io.nexuscrm.sfcrm.record-mutation.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.nexuscrm.sfcrm.handler.record-mutation.v1", RequiredRuntimeCapabilities: []string{recordmutationv1.CapabilityName}},
	}
	request, err := config.BuildRequest(release)
	if err != nil {
		t.Fatal(err)
	}
	if len(request.Capabilities) != 5 || !slices.Equal(request.Operation.GrantedCapabilities, release.Package.Manifest.RequiredEngineCapabilities) {
		t.Fatalf("five-capability request = %#v", request)
	}
	if request.Capabilities[1].RuntimeCapabilityID != recordmutationv1.CapabilityName ||
		request.Capabilities[2].RuntimeCapabilityID != recyclebinv1.CapabilityName ||
		request.Capabilities[3].RuntimeCapabilityID != relatedcountsv1.CapabilityName {
		t.Fatalf("canonical capability order = %#v", request.Capabilities)
	}

	undeclared := release
	undeclared.Package.Manifest.RequiredEngineCapabilities = undeclared.Package.Manifest.RequiredEngineCapabilities[:3]
	undeclared.Package.Manifest.ProvidedCapabilities = undeclared.Package.Manifest.ProvidedCapabilities[:3]
	if _, err := config.BuildRequest(undeclared); err == nil || !strings.Contains(err.Error(), "cover every provided") {
		t.Fatalf("undeclared mutation selection error = %v", err)
	}
}

func TestBuildRequestV4SeparatesCatalogConsumerGrantsFromEngineGrants(t *testing.T) {
	raw := strings.Replace(fixtureConfigJSON(), ConfigSchemaV1, ConfigSchemaV4, 1)
	raw = strings.Replace(raw,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`",`,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`,
		1,
	)
	raw = strings.Replace(raw,
		`"workloadId":"workload-test",`,
		`"workloadId":"workload-test",
"authorizedWorkloadIds":["workload-integration","workload-test"],
"consumerGrants":[{"authorizedWorkloadId":"workload-integration","capabilityId":"`+consumerv1.IntegrationBindingCapabilityName+`"},{"authorizedWorkloadId":"workload-test","capabilityId":"`+consumerv1.AppMetadataCapabilityName+`"},{"authorizedWorkloadId":"workload-test","capabilityId":"`+consumerv1.AppMetadataV2CapabilityName+`"},{"authorizedWorkloadId":"workload-test","capabilityId":"`+pagelayoutresolutionv1.CapabilityName+`"}],`,
		1,
	)
	raw = strings.Replace(raw,
		`"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",`,
		`"capabilities":[{"appCapabilityId":"app.record-access.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1","runtimeCapabilityId":"runtime.records.evaluate-access.v1"},{"appCapabilityId":"app.search.v1","handlerRef":"io.nexuscrm.sfcrm.handler.search.v1","runtimeCapabilityId":"runtime.records.query.v1"},{"appCapabilityId":"app.search-scope-order.v1","handlerRef":"io.nexuscrm.sfcrm.handler.search-scope-order.v1","runtimeCapabilityId":"catalog.consumer.app-metadata-view.v1"},{"appCapabilityId":"app.ui-apps.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui-apps.v1","runtimeCapabilityId":"catalog.consumer.app-metadata-view.v2"}],`,
		1,
	)
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatal(err)
	}
	release := fixtureRegisteredRelease()
	release.Package.Manifest.RequiredEngineCapabilities = []string{
		consumerv1.AppMetadataCapabilityName,
		consumerv1.AppMetadataV2CapabilityName,
		consumerv1.IntegrationBindingCapabilityName,
		pagelayoutresolutionv1.CapabilityName,
		"runtime.commands.accept.v1",
		recordaccessv1.CapabilityName,
		queryv1.CapabilityName,
	}
	release.Package.Manifest.ProvidedCapabilities = append(
		release.Package.Manifest.ProvidedCapabilities,
		appauthority.ProvidedCapability{
			CapabilityID: "app.search.v1",
			Kind:         appauthority.CapabilityKindServiceHandler,
			HandlerRef:   "io.nexuscrm.sfcrm.handler.search.v1",
			RequiredRuntimeCapabilities: []string{
				consumerv1.AppMetadataCapabilityName,
				consumerv1.AppMetadataV2CapabilityName,
				pagelayoutresolutionv1.CapabilityName,
				queryv1.CapabilityName,
			},
		},
		appauthority.ProvidedCapability{
			CapabilityID: "app.search-scope-order.v1",
			Kind:         appauthority.CapabilityKindServiceHandler,
			HandlerRef:   "io.nexuscrm.sfcrm.handler.search-scope-order.v1",
			RequiredRuntimeCapabilities: []string{
				consumerv1.AppMetadataCapabilityName,
				queryv1.CapabilityName,
			},
		},
		appauthority.ProvidedCapability{
			CapabilityID: "app.ui-apps.v1",
			Kind:         appauthority.CapabilityKindServiceHandler,
			HandlerRef:   "io.nexuscrm.sfcrm.handler.ui-apps.v1",
			RequiredRuntimeCapabilities: []string{
				consumerv1.AppMetadataV2CapabilityName,
			},
		},
	)
	request, err := config.BuildRequest(release)
	if err != nil {
		t.Fatal(err)
	}
	if err := validateRequestAgainstRelease(request, release); err != nil {
		t.Fatalf("reconcile release closure: %v", err)
	}
	if err := validateReleaseCapabilityClosureV4(
		release.Package.Manifest.ProvidedCapabilities,
		release.Package.Manifest.RequiredEngineCapabilities,
		request.Capabilities[:len(request.Capabilities)-1],
	); err == nil || !strings.Contains(err.Error(), "cover every provided") {
		t.Fatalf("incomplete v4 handler closure error = %v", err)
	}
	if !slices.Equal(
		request.Operation.GrantedCapabilities,
		release.Package.Manifest.RequiredEngineCapabilities,
	) {
		t.Fatalf(
			"engine grants = %#v, want release requirements %#v",
			request.Operation.GrantedCapabilities,
			release.Package.Manifest.RequiredEngineCapabilities,
		)
	}
	if len(request.Operation.SelectedHandlerCapabilities) != 0 {
		t.Fatalf(
			"legacy all-handler selection changed request identity: %#v",
			request.Operation.SelectedHandlerCapabilities,
		)
	}
	if len(request.Operation.ConsumerGrants) != 4 ||
		!slices.Contains(
			request.Operation.GrantedCapabilities,
			consumerv1.AppMetadataCapabilityName,
		) ||
		!slices.Contains(
			request.Operation.GrantedCapabilities,
			consumerv1.IntegrationBindingCapabilityName,
		) {
		t.Fatalf("consumer/engine grant separation = %#v", request.Operation)
	}
	withoutV2Grant := config
	withoutV2Grant.ConsumerGrants = slices.DeleteFunc(
		slices.Clone(config.ConsumerGrants),
		func(grant ConsumerGrantConfig) bool {
			return grant.AuthorizedWorkloadID == config.WorkloadID &&
				grant.CapabilityID == consumerv1.AppMetadataV2CapabilityName
		},
	)
	withoutV2Request, err := withoutV2Grant.BuildRequest(release)
	if err != nil || slices.ContainsFunc(
		withoutV2Request.Operation.ConsumerGrants,
		func(grant catalogapp.ConsumerGrant) bool {
			return grant.AuthorizedWorkloadID == config.WorkloadID &&
				grant.CapabilityID == consumerv1.AppMetadataV2CapabilityName
		},
	) {
		t.Fatalf("reconcile retained derived V2 disclosure grant = %#v, %v", withoutV2Request.Operation.ConsumerGrants, err)
	}
	withoutPageLayoutGrant := config
	withoutPageLayoutGrant.ConsumerGrants = slices.DeleteFunc(
		slices.Clone(config.ConsumerGrants),
		func(grant ConsumerGrantConfig) bool {
			return grant.AuthorizedWorkloadID == config.WorkloadID &&
				grant.CapabilityID == pagelayoutresolutionv1.CapabilityName
		},
	)
	withoutPageLayoutRequest, err := withoutPageLayoutGrant.BuildRequest(release)
	if err != nil || slices.ContainsFunc(
		withoutPageLayoutRequest.Operation.ConsumerGrants,
		func(grant catalogapp.ConsumerGrant) bool {
			return grant.AuthorizedWorkloadID == config.WorkloadID &&
				grant.CapabilityID == pagelayoutresolutionv1.CapabilityName
		},
	) {
		t.Fatalf("reconcile retained derived PageLayout disclosure grant = %#v, %v", withoutPageLayoutRequest.Operation.ConsumerGrants, err)
	}
	if request.Operation.HandlerWorkloadID != config.WorkloadID {
		t.Fatalf("handler workload = %q, want %q", request.Operation.HandlerWorkloadID, config.WorkloadID)
	}
	refresh := config
	refresh.OperationKind = catalogapp.OperationUpgrade
	refresh.AppInstallationID = "01900000-0000-7000-8000-000000000001"
	refresh.RefreshActiveRelease = true
	refreshRequest, err := refresh.BuildRequest(release)
	if err != nil {
		t.Fatal(err)
	}
	if !refreshRequest.Operation.RefreshActiveRelease {
		t.Fatal("active release refresh intent was not carried into the Catalog operation")
	}
}

func TestConfigSelectsRuntimeMetadataCapabilityForConfiguredWorkloadOnly(t *testing.T) {
	config := Config{
		WorkloadID: "layout-worker",
		ConsumerGrants: []ConsumerGrantConfig{
			{AuthorizedWorkloadID: "another-worker", CapabilityID: layoutauthoringv1.CapabilityName},
		},
	}
	if config.SelectsRuntimeMetadataCapability(layoutauthoringv1.CapabilityName) {
		t.Fatal("foreign-workload grant selected Layout authoring")
	}
	config.ConsumerGrants = append(config.ConsumerGrants, ConsumerGrantConfig{
		AuthorizedWorkloadID: config.WorkloadID, CapabilityID: layoutauthoringv1.CapabilityName,
	})
	if !config.SelectsRuntimeMetadataCapability(layoutauthoringv1.CapabilityName) {
		t.Fatal("configured workload grant did not select Layout authoring")
	}
	config.ConsumerGrants = nil
	config.Capabilities = []CapabilityConfig{{RuntimeCapabilityID: layoutauthoringv1.CapabilityName}}
	if !config.SelectsRuntimeMetadataCapability(layoutauthoringv1.CapabilityName) {
		t.Fatal("handler capability did not select Layout authoring")
	}
}

func TestV4ClosureLeavesWorkloadOnlyHandlerOutsideCatalogSelection(t *testing.T) {
	const (
		queryCapability   = "runtime.records.query.v1"
		sessionCapability = "runtime.identity.session-lifecycle.v1"
	)
	provided := []appauthority.ProvidedCapability{
		{
			CapabilityID: "app.query.v1", Kind: appauthority.CapabilityKindServiceHandler,
			HandlerRef: "query", RequiredRuntimeCapabilities: []string{queryCapability},
		},
		{
			CapabilityID: "app.scim.v1", Kind: appauthority.CapabilityKindServiceHandler,
			HandlerRef: "scim", RequiredRuntimeCapabilities: []string{
				sessionCapability, queryCapability,
			},
		},
	}
	selections := []CapabilitySelection{{
		AppCapabilityID: "app.query.v1", HandlerRef: "query",
		RuntimeCapabilityID: queryCapability,
	}}
	if selected := durableSelectedHandlerCapabilityIDs(provided, selections); !slices.Equal(
		selected, []string{"app.query.v1"},
	) {
		t.Fatalf("durable workload-only selection = %#v", selected)
	}
	allSelections := append(slices.Clone(selections), CapabilitySelection{
		AppCapabilityID: "app.scim.v1", HandlerRef: "scim",
		RuntimeCapabilityID: sessionCapability,
	})
	if selected := durableSelectedHandlerCapabilityIDs(provided, allSelections); len(selected) != 0 {
		t.Fatalf("legacy all-handler selection = %#v, want empty", selected)
	}
	if err := validateReleaseCapabilityClosureV4(
		provided,
		[]string{queryCapability, sessionCapability},
		selections,
	); err != nil {
		t.Fatalf("workload-only handler closure: %v", err)
	}

	selectedWithMissingDependency := []CapabilitySelection{{
		AppCapabilityID: "app.scim.v1", HandlerRef: "scim",
		RuntimeCapabilityID: sessionCapability,
	}}
	if err := validateReleaseCapabilityClosureV4(
		provided,
		[]string{queryCapability, sessionCapability},
		selectedWithMissingDependency,
	); err == nil || !strings.Contains(err.Error(), "no Runtime serving selection") {
		t.Fatalf("selected handler incomplete Runtime closure error = %v", err)
	}

	withoutWorkloadDependency := slices.Clone(provided)
	withoutWorkloadDependency[1].RequiredRuntimeCapabilities = []string{queryCapability}
	if err := validateReleaseCapabilityClosureV4(
		withoutWorkloadDependency,
		[]string{queryCapability},
		selections,
	); err == nil || !strings.Contains(err.Error(), "cover every provided") {
		t.Fatalf("unselected Catalog handler error = %v", err)
	}

	if err := validateReleaseCapabilityClosureV4(
		provided,
		[]string{queryCapability},
		selections,
	); err == nil || !strings.Contains(err.Error(), "absent from the verified AppRelease") {
		t.Fatalf("unverified workload dependency error = %v", err)
	}
}

func TestV4ClosureAcceptsPermissionAuthoringCompatibilityFenceWithoutServingSelection(t *testing.T) {
	provided := []appauthority.ProvidedCapability{{
		CapabilityID: "app.permission-authoring.v1",
		Kind:         appauthority.CapabilityKindServiceHandler,
		HandlerRef:   "permission-authoring",
		RequiredRuntimeCapabilities: []string{
			permissionauthoringv1.RecordTypeObjectGroupsCapabilityName,
			permissionauthoringv1.CapabilityName,
		},
	}}
	selections := []CapabilitySelection{{
		AppCapabilityID:     "app.permission-authoring.v1",
		HandlerRef:          "permission-authoring",
		RuntimeCapabilityID: permissionauthoringv1.CapabilityName,
	}}
	if err := validateReleaseCapabilityClosureV4(
		provided,
		[]string{
			permissionauthoringv1.CapabilityName,
			permissionauthoringv1.RecordTypeObjectGroupsCapabilityName,
		},
		selections,
	); err != nil {
		t.Fatalf("Permission authoring compatibility fence closure: %v", err)
	}
}

func TestBuildRequestV4AcceptsOneServingAndOneWorkloadOnlyHandler(t *testing.T) {
	const sessionCapability = "runtime.identity.session-lifecycle.v1"
	raw := strings.Replace(fixtureConfigJSON(), ConfigSchemaV1, ConfigSchemaV4, 1)
	raw = strings.Replace(
		raw,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`",`,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`,
		1,
	)
	raw = strings.Replace(
		raw,
		`"workloadId":"workload-test",`,
		`"workloadId":"workload-test","authorizedWorkloadIds":["workload-test"],`,
		1,
	)
	raw = strings.Replace(
		raw,
		`"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",`,
		`"capabilities":[{"appCapabilityId":"app.record-access.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1","runtimeCapabilityId":"runtime.records.evaluate-access.v1"}],`,
		1,
	)
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatalf("parse single-selection v4 config: %v", err)
	}

	release := fixtureRegisteredRelease()
	release.Package.Manifest.RequiredEngineCapabilities = append(
		release.Package.Manifest.RequiredEngineCapabilities,
		sessionCapability,
	)
	release.Package.Manifest.ProvidedCapabilities = append(
		release.Package.Manifest.ProvidedCapabilities,
		appauthority.ProvidedCapability{
			CapabilityID: "app.scim.v1", Kind: appauthority.CapabilityKindServiceHandler,
			HandlerRef:                  "scim",
			RequiredRuntimeCapabilities: []string{sessionCapability},
		},
	)
	request, err := config.BuildRequest(release)
	if err != nil {
		t.Fatalf("build single-serving-handler v4 request: %v", err)
	}
	if len(request.Capabilities) != 1 || !slices.Equal(
		request.Operation.SelectedHandlerCapabilities,
		[]string{"app.record-access.v1"},
	) {
		t.Fatalf("single-serving-handler request = %#v", request)
	}
	if len(request.Operation.WorkloadHandlerAdmissions) != 1 ||
		request.Operation.WorkloadHandlerAdmissions[0].CapabilityID != "app.scim.v1" ||
		request.Operation.WorkloadHandlerAdmissions[0].AuthorizedWorkloadID != "workload-test" ||
		!slices.Equal(
			request.Operation.WorkloadHandlerAdmissions[0].RequiredRuntimeCapabilities,
			[]string{sessionCapability},
		) {
		t.Fatalf("workload handler authority = %#v", request.Operation.WorkloadHandlerAdmissions)
	}
	// The selected handler now composes the same workload dependency. Both
	// config validation and transaction-time release validation must retain it.
	release.Package.Manifest.ProvidedCapabilities[0].RequiredRuntimeCapabilities = append(
		release.Package.Manifest.ProvidedCapabilities[0].RequiredRuntimeCapabilities, sessionCapability)
	config.HandlerWorkloadCapabilities = []string{sessionCapability}
	request, err = config.BuildRequest(release)
	if err != nil {
		t.Fatalf("build shared workload request: %v", err)
	}
	config.HandlerWorkloadCapabilities[0] = "runtime.changed.v1"
	if err := validateRequestAgainstRelease(request, release); err != nil {
		t.Fatalf("shared workload authority was lost before reconcile: %v", err)
	}

}

func TestParseConfigScopesActiveReleaseRefreshToV4Upgrade(t *testing.T) {
	legacy := strings.Replace(
		fixtureConfigJSON(),
		`"operationKind":"PREPARE",`,
		`"operationKind":"UPGRADE","refreshActiveRelease":true,`,
		1,
	)
	legacy = strings.Replace(
		legacy,
		`"appInstallationId":"",`,
		`"appInstallationId":"01900000-0000-7000-8000-000000000001",`,
		1,
	)
	if _, err := ParseConfig([]byte(legacy)); err == nil ||
		!strings.Contains(err.Error(), "requires config schema v4") {
		t.Fatalf("legacy active release refresh error = %v", err)
	}

	notUpgrade := strings.Replace(legacy, ConfigSchemaV1, ConfigSchemaV4, 1)
	notUpgrade = strings.Replace(
		notUpgrade,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`",`,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`,
		1,
	)
	notUpgrade = strings.Replace(notUpgrade, `"operationKind":"UPGRADE"`, `"operationKind":"ROLLBACK"`, 1)
	if _, err := ParseConfig([]byte(notUpgrade)); err == nil ||
		!strings.Contains(err.Error(), "requires operationKind UPGRADE") {
		t.Fatalf("non-upgrade active release refresh error = %v", err)
	}
}

func TestIntegrationBindingSelectionsCarryExactRuntimeCommand(t *testing.T) {
	config := Config{IntegrationBindings: []IntegrationBindingConfig{{
		AuthorizedWorkloadID: "workload-integration",
		Kind:                 consumerv1.BindingConnectorInboundRoute,
		OwnerPack:            "connector-pack",
		PackInstallationID:   "pack-install-1",
		PackPayloadDigest:    "nexus-pack-sha256-v1:" + strings.Repeat("a", 64),
		BusinessKey:          "connector-pack/contact.updated",
		DefinitionVersion:    1,
		CredentialRefs:       []string{},
		RuntimeCommand: &RuntimeCommandBindingConfig{
			CommandKind:          "runtime.flow.execute",
			PayloadFormatVersion: 1,
			DefinitionRef:        "ContactUpdated",
			DefinitionVersion:    "flow-version-1",
		},
	}}}
	selections := config.integrationBindingSelections()
	if len(selections) != 1 ||
		selections[0].Binding.RuntimeCommand == nil ||
		selections[0].Binding.RuntimeCommand.DefinitionRef != "ContactUpdated" {
		t.Fatalf("Runtime command binding = %#v", selections)
	}
	config.IntegrationBindings[0].RuntimeCommand.DefinitionRef = "changed"
	if selections[0].Binding.RuntimeCommand.DefinitionRef != "ContactUpdated" {
		t.Fatal("Runtime command binding retained mutable config ownership")
	}
}

func TestBuildRequestV4LeavesHandlerCatalogRequirementsToCatalog(
	t *testing.T,
) {
	raw := strings.Replace(
		fixtureConfigJSON(),
		ConfigSchemaV1,
		ConfigSchemaV4,
		1,
	)
	raw = strings.Replace(
		raw,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`",`,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`,
		1,
	)
	raw = strings.Replace(
		raw,
		`"workloadId":"workload-test",`,
		`"workloadId":"workload-test",
"authorizedWorkloadIds":["workload-integration","workload-test"],
"consumerGrants":[{"authorizedWorkloadId":"workload-integration","capabilityId":"`+consumerv1.AppMetadataV2CapabilityName+`"}],`,
		1,
	)
	raw = strings.Replace(
		raw,
		`"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",`,
		`"capabilities":[{"appCapabilityId":"app.record-access.v1","handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1","runtimeCapabilityId":"runtime.records.evaluate-access.v1"},{"appCapabilityId":"app.search.v1","handlerRef":"io.nexuscrm.sfcrm.handler.search.v1","runtimeCapabilityId":"runtime.records.query.v1"}],`,
		1,
	)
	config, err := ParseConfig([]byte(raw))
	if err != nil {
		t.Fatal(err)
	}
	release := fixtureRegisteredRelease()
	release.Package.Manifest.RequiredEngineCapabilities = []string{
		consumerv1.AppMetadataV2CapabilityName,
		recordaccessv1.CapabilityName,
		queryv1.CapabilityName,
	}
	release.Package.Manifest.ProvidedCapabilities = append(
		release.Package.Manifest.ProvidedCapabilities,
		appauthority.ProvidedCapability{
			CapabilityID: "app.search.v1",
			Kind:         appauthority.CapabilityKindServiceHandler,
			HandlerRef:   "io.nexuscrm.sfcrm.handler.search.v1",
			RequiredRuntimeCapabilities: []string{
				consumerv1.AppMetadataV2CapabilityName,
				queryv1.CapabilityName,
			},
		},
	)
	request, err := config.BuildRequest(release)
	if err != nil || len(request.Operation.ConsumerGrants) != 1 ||
		request.Operation.ConsumerGrants[0].AuthorizedWorkloadID != "workload-integration" ||
		request.Operation.HandlerWorkloadID != "workload-test" {
		t.Fatalf("reconcile-owned grants = %#v, handler workload = %q, %v", request.Operation.ConsumerGrants, request.Operation.HandlerWorkloadID, err)
	}

	config.ConsumerGrants[0].AuthorizedWorkloadID = "workload-test"
	config.Capabilities[1].RuntimeCapabilityID = relatedcountsv1.CapabilityName
	if _, err := config.BuildRequest(release); err == nil ||
		!strings.Contains(err.Error(), "does not match the verified AppRelease") {
		t.Fatalf("unverified service selection error = %v", err)
	}
}

func fixtureRegisteredRelease() catalogapp.RegisteredRelease {
	return catalogapp.RegisteredRelease{
		AppID: "io.nexuscrm.sfcrm", AppReleaseDigest: digestOf('a'),
		Package: appauthority.VerifiedPackage{Package: appauthority.Package{Manifest: appauthority.AppManifest{
			AppID:                      "io.nexuscrm.sfcrm",
			RequiredEngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
			ProvidedCapabilities: []appauthority.ProvidedCapability{{
				CapabilityID: "app.record-access.v1", Kind: appauthority.CapabilityKindServiceHandler,
				HandlerRef:                  "io.nexuscrm.sfcrm.handler.ui.record-access.v1",
				RequiredRuntimeCapabilities: []string{"runtime.records.evaluate-access.v1"},
			}},
			FrontendCompatibility: appauthority.FrontendCompatibility{Mode: appauthority.FrontendModeRequired},
		}}},
	}
}

func TestMarshalOutputIsDeterministic(t *testing.T) {
	output := Output{
		Schema: OutputSchemaV1, OperationID: "operation-test", OrgID: "org-test",
		AppInstallationID: "installation-test", AppReleaseDigest: digestOf('a'),
		AppCapabilityID: "app.record-access.v1", RuntimeCapabilityID: "runtime.records.evaluate-access.v1",
		HandlerBindingID: "binding-test", WorkloadID: "workload-test", ServiceReleaseDigest: digestOf('b'),
		OwnershipEpoch: 1, CatalogRevision: 2, SchemaGeneration: 3, EngineGeneration: 4, SecurityEpoch: 5,
		CatalogSnapshotDigest: digestOf('c'), RuntimeMetadataViewDigest: digestOf('d'),
		CompatibilityReceiptDigest: digestOf('e'),
		FrontendBindingID:          "frontend-binding-test", FrontendReleaseDigest: digestOf('f'),
		FrontendReceiptDigest: digestOf('9'),
	}
	first, firstDigest, err := MarshalOutput(output)
	if err != nil {
		t.Fatal(err)
	}
	second, secondDigest, err := MarshalOutput(output)
	if err != nil {
		t.Fatal(err)
	}
	if string(first) != string(second) || firstDigest != secondDigest || !strings.HasPrefix(firstDigest, "sha256:") {
		t.Fatalf("non-deterministic output: %s %s / %s %s", first, firstDigest, second, secondDigest)
	}
	output.FrontendReceiptDigest = ""
	if _, _, err := MarshalOutput(output); err == nil || !strings.Contains(err.Error(), "frontend facts") {
		t.Fatalf("partial frontend output error = %v", err)
	}
	output.FrontendReceiptDigest = digestOf('9')
	output.Schema = OutputSchemaV2
	if _, _, err := MarshalOutput(output); err == nil || !strings.Contains(err.Error(), "metadataSourceDigest") {
		t.Fatalf("v2 output missing metadata source error = %v", err)
	}
	output.MetadataSourceDigest = digestOf('8')
	if _, _, err := MarshalOutput(output); err != nil {
		t.Fatalf("v2 output: %v", err)
	}
	v3 := output
	v3.Schema = OutputSchemaV3
	v3.AppCapabilityID, v3.RuntimeCapabilityID, v3.HandlerBindingID = "", "", ""
	v3.RuntimeMetadataViewDigest, v3.CompatibilityReceiptDigest, v3.OwnershipEpoch = "", "", 0
	v3.Capabilities = []CapabilityOutput{
		{AppCapabilityID: "io.nexuscrm.sfcrm.query.v1", RuntimeCapabilityID: "runtime.records.query.v1", HandlerBindingID: "binding-query", OwnershipEpoch: 1, RuntimeMetadataViewDigest: digestOf('4'), CompatibilityReceiptDigest: digestOf('5')},
		{AppCapabilityID: "io.nexuscrm.sfcrm.related-list-counts.v1", RuntimeCapabilityID: relatedcountsv1.CapabilityName, HandlerBindingID: "binding-related-counts", OwnershipEpoch: 1, RuntimeMetadataViewDigest: digestOf('8'), CompatibilityReceiptDigest: digestOf('3')},
		{AppCapabilityID: "io.nexuscrm.sfcrm.ui.record-access.v1", RuntimeCapabilityID: "runtime.records.evaluate-access.v1", HandlerBindingID: "binding-access", OwnershipEpoch: 1, RuntimeMetadataViewDigest: digestOf('6'), CompatibilityReceiptDigest: digestOf('7')},
	}
	if _, _, err := MarshalOutput(v3); err != nil {
		t.Fatalf("v3 output: %v", err)
	}
	v3.LayoutAuthoringViewRef = &layoutauthoringv1.CatalogViewRef{
		CatalogRevision:     v3.CatalogRevision,
		AuthoringViewDigest: digestOf('1'),
	}
	rawWithRef, _, err := MarshalOutput(v3)
	if err != nil {
		t.Fatalf("v3 Layout authoring output: %v", err)
	}
	if strings.Contains(string(rawWithRef), "layoutAuthoringViewRef") ||
		strings.Contains(string(rawWithRef), "authoring_view_digest") {
		t.Fatalf("in-process Layout authoring ref changed the closed v3 handoff: %s", rawWithRef)
	}
	mismatchedRef := v3
	mismatchedRef.LayoutAuthoringViewRef = &layoutauthoringv1.CatalogViewRef{
		CatalogRevision:     v3.CatalogRevision + 1,
		AuthoringViewDigest: digestOf('1'),
	}
	if _, _, err := MarshalOutput(mismatchedRef); err == nil || !strings.Contains(err.Error(), "revision disagrees") {
		t.Fatalf("mismatched Layout authoring view ref error = %v", err)
	}
	legacyWithRef := output
	legacyWithRef.LayoutAuthoringViewRef = v3.LayoutAuthoringViewRef
	if _, _, err := MarshalOutput(legacyWithRef); err == nil || !strings.Contains(err.Error(), "v1/v2") {
		t.Fatalf("legacy Layout authoring view ref error = %v", err)
	}
	v3.LayoutAuthoringViewRef = nil
	unsupported := v3
	unsupported.Capabilities = append([]CapabilityOutput(nil), v3.Capabilities...)
	unsupported.Capabilities[1].RuntimeCapabilityID = "runtime.records.unsupported.v1"
	if _, _, err := MarshalOutput(unsupported); err == nil || !strings.Contains(err.Error(), "does not support") {
		t.Fatalf("unsupported v3 output capability error = %v", err)
	}
	v3.Capabilities = v3.Capabilities[:1]
	if _, _, err := MarshalOutput(v3); err != nil {
		t.Fatalf("single-fact v3 output: %v", err)
	}
	v3.Capabilities = nil
	if _, _, err := MarshalOutput(v3); err == nil || !strings.Contains(err.Error(), "at least one") {
		t.Fatalf("empty v3 output error = %v", err)
	}
}

func fixtureConfigJSON() string {
	key := base64.StdEncoding.EncodeToString([]byte("public-key"))
	return `{
"schema":"nexuscrm.catalog-reconcile-config/v1",
"orgId":"org-test",
"appId":"io.nexuscrm.sfcrm",
"expectedAppReleaseDigest":"` + digestOf('a') + `",
"releaseImport":{"packagePath":"/artifacts/release.json","attestationPath":"/artifacts/attestation.json","publisher":{"publisherId":"io.nexuscrm","keys":[{"keyId":"release-1","algorithm":"Ed25519","publicKeyBase64":"` + key + `","validFrom":"2026-01-01T00:00:00Z","validUntil":"2027-01-01T00:00:00Z"}]}},
"artifactStoreRoot":"/artifacts/catalog",
"hostCompatibility":{"protocols":[],"engineCapabilities":["runtime.records.evaluate-access.v1"],"componentKinds":[]},
"idempotencyKey":"d1-test",
"operationKind":"PREPARE",
"appInstallationId":"",
"expectedCatalogRevision":0,
"expectedSecurityEpoch":0,
"runtimeAudience":"runtime-test",
"workloadId":"workload-test",
"appCapabilityId":"app.record-access.v1",
"handlerRef":"io.nexuscrm.sfcrm.handler.ui.record-access.v1",
"runtimeCapabilityId":"runtime.records.evaluate-access.v1",
"serviceReleaseDigest":"` + digestOf('b') + `",
"frontend":{"releaseDigest":"` + digestOf('c') + `","compatibilityReceiptDigest":"` + digestOf('d') + `"},
"schemaGeneration":3,
"engineGeneration":4,
"resolvedDependencies":[]
}`
}

func digestOf(value byte) string {
	return "sha256:" + strings.Repeat(string(value), 64)
}

func TestV4SelectedHandlerRequiresExplicitSharedWorkloadDependency(t *testing.T) {
	const query = "runtime.records.query.v1"
	const session = "runtime.identity.session-lifecycle.v1"
	provided := []appauthority.ProvidedCapability{
		{CapabilityID: "app.composite.v1", Kind: appauthority.CapabilityKindServiceHandler,
			HandlerRef: "composite", RequiredRuntimeCapabilities: []string{query, session}},
		{CapabilityID: "app.session.v1", Kind: appauthority.CapabilityKindServiceHandler,
			HandlerRef: "session", RequiredRuntimeCapabilities: []string{session}},
	}
	selections := []CapabilitySelection{{AppCapabilityID: "app.composite.v1", HandlerRef: "composite", RuntimeCapabilityID: query}}
	required := []string{query, session}
	if err := validateReleaseCapabilityClosureV4(provided, required, selections, session); err != nil {
		t.Fatalf("explicit shared workload dependency: %v", err)
	}
	for _, workload := range [][]string{nil, {query}, {session, session}, {"runtime.unknown.v1"}} {
		if err := validateReleaseCapabilityClosureV4(provided, required, selections, workload...); err == nil {
			t.Fatalf("accepted missing, conflicting or unverified workload authority: %v", workload)
		}
	}
	// An omitted alias of the concrete serving capability is still rejected by
	// the owner that has the actual serving selections, not inferred by storage.
	provided[1].RequiredRuntimeCapabilities = []string{query}
	if err := validateReleaseCapabilityClosureV4(provided, required, selections, session); err == nil {
		t.Fatal("accepted omission of a fully Catalog-selected handler")
	}
}
