package catalogreconcile

import (
	"context"
	"crypto/sha256"
	"errors"
	"slices"
	"strings"
	"testing"

	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
	"github.com/pingcap-inc/nexuscrm-controlplane/releaseartifact"
)

func TestParseStoppedSourceConfigOwnsAClosedUnsignedWrapper(t *testing.T) {
	raw := fixtureStoppedSourceConfigJSON(t)
	config, err := ParseStoppedSourceConfig([]byte(raw))
	if err != nil {
		t.Fatalf("parse stopped-source config: %v", err)
	}
	if config.Schema != StoppedSourceConfigSchemaV1 ||
		config.SourceRelease.PackagePath != "/artifacts/app-package.json" ||
		config.SourceRelease.AppID != "io.nexuscrm.sfcrm" ||
		config.SourceRelease.ExpectedAppReleaseDigest != digestOf('a') ||
		config.SourceRelease.ExpectedPackageContentDigest != digestOf('6') ||
		config.SourceRelease.SourceRepository != "pingcap-inc/nexuscrm-app-packs" ||
		config.SourceRelease.SourceCommit != strings.Repeat("1", 40) {
		t.Fatalf("stopped-source coordinates = %#v", config.SourceRelease)
	}
	if config.Reconcile.Schema != ConfigSchemaV4 || config.Reconcile.ReleaseImport != nil {
		t.Fatalf("embedded reconcile config = %#v", config.Reconcile)
	}

	tests := []struct {
		name   string
		mutate func(string) string
	}{
		{
			name: "duplicate root field",
			mutate: func(value string) string {
				return strings.Replace(value, `"schema":"`+StoppedSourceConfigSchemaV1+`",`, `"schema":"`+StoppedSourceConfigSchemaV1+`","schema":"`+StoppedSourceConfigSchemaV1+`",`, 1)
			},
		},
		{
			name: "unknown root field",
			mutate: func(value string) string {
				return strings.Replace(value, `"schema":"`+StoppedSourceConfigSchemaV1+`",`, `"schema":"`+StoppedSourceConfigSchemaV1+`","publisher":"forbidden",`, 1)
			},
		},
		{
			name: "signed source field",
			mutate: func(value string) string {
				return strings.Replace(value, `"packagePath":"/artifacts/app-package.json",`, `"packagePath":"/artifacts/app-package.json","attestationPath":"/artifacts/attestation.json",`, 1)
			},
		},
		{
			name:   "trailing content",
			mutate: func(value string) string { return value + `{}` },
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if _, err := ParseStoppedSourceConfig([]byte(test.mutate(raw))); err == nil {
				t.Fatal("ambiguous stopped-source wrapper was accepted")
			}
		})
	}
}

func TestParseStoppedSourceConfigRejectsSignedImportAndCoordinateDisagreement(t *testing.T) {
	raw := fixtureStoppedSourceConfigJSON(t)
	explicitNull := strings.Replace(raw, `"artifactStoreRoot":`, `"releaseImport":null,"artifactStoreRoot":`, 1)
	if _, err := ParseStoppedSourceConfig([]byte(explicitNull)); err == nil || !strings.Contains(err.Error(), "releaseImport") {
		t.Fatalf("explicit null releaseImport error = %v", err)
	}
	tests := []struct {
		name   string
		mutate func(string) string
	}{
		{
			name: "embedded signed release import",
			mutate: func(value string) string {
				return strings.Replace(value, `"artifactStoreRoot":`, `"releaseImport":{"packagePath":"/signed/package.json","attestationPath":"/signed/attestation.json","publisher":{"publisherId":"io.nexuscrm","keys":[]}},"artifactStoreRoot":`, 1)
			},
		},
		{
			name: "App identity disagreement",
			mutate: func(value string) string {
				return strings.Replace(value, `"appId":"io.nexuscrm.sfcrm",`, `"appId":"io.example.other",`, 1)
			},
		},
		{
			name: "release digest disagreement",
			mutate: func(value string) string {
				return strings.Replace(value, `"expectedAppReleaseDigest":"`+digestOf('a')+`",`, `"expectedAppReleaseDigest":"`+digestOf('9')+`",`, 1)
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if _, err := ParseStoppedSourceConfig([]byte(test.mutate(raw))); err == nil {
				t.Fatal("disagreeing stopped-source authority was accepted")
			}
		})
	}
}

func TestStoppedSourceConfigBuildsExactCatalogInputFromCallerBytes(t *testing.T) {
	config, err := ParseStoppedSourceConfig([]byte(fixtureStoppedSourceConfigJSON(t)))
	if err != nil {
		t.Fatal(err)
	}
	packageBytes := []byte("exact untrusted carrier bytes")
	input, err := config.ReleaseInput(packageBytes)
	if err != nil {
		t.Fatalf("build stopped-source release input: %v", err)
	}
	if input.AppID != config.SourceRelease.AppID ||
		input.ExpectedAppReleaseDigest != config.SourceRelease.ExpectedAppReleaseDigest ||
		input.ExpectedPackageContentDigest != config.SourceRelease.ExpectedPackageContentDigest ||
		input.SourceRepository != config.SourceRelease.SourceRepository ||
		input.SourceCommit != config.SourceRelease.SourceCommit ||
		string(input.PackageBytes) != string(packageBytes) {
		t.Fatalf("release input = %#v", input)
	}
	if len(input.Compatibility.EngineCapabilities) != len(config.Reconcile.HostCompatibility.EngineCapabilities) {
		t.Fatalf("host compatibility = %#v", input.Compatibility)
	}
	packageBytes[0] = 'X'
	if string(input.PackageBytes) != "exact untrusted carrier bytes" {
		t.Fatalf("release input retained caller-owned package bytes: %q", input.PackageBytes)
	}
	var _ catalogapp.StoppedSourceReleaseInput = input
}

type fakeStoppedSourceResolver struct {
	release           catalogapp.RegisteredRelease
	readErr           error
	stoppedRelease    catalogapp.RegisteredRelease
	stoppedReadErr    error
	importedRelease   catalogapp.RegisteredRelease
	importedReference catalogapp.StoppedSourceReleaseReference
	importErr         error
	reads             int
	stoppedReads      int
	imports           int
	readAppID         string
	readDigest        string
	readReference     catalogapp.StoppedSourceReleaseReference
	importInput       catalogapp.StoppedSourceReleaseInput
}

func (f *fakeStoppedSourceResolver) ReadRelease(
	_ context.Context,
	appID, digest string,
) (catalogapp.RegisteredRelease, error) {
	f.reads++
	f.readAppID, f.readDigest = appID, digest
	return f.release, f.readErr
}

func (f *fakeStoppedSourceResolver) ReadStoppedSourceRelease(
	_ context.Context,
	reference catalogapp.StoppedSourceReleaseReference,
) (catalogapp.RegisteredRelease, error) {
	f.stoppedReads++
	f.readReference = reference
	return f.stoppedRelease, f.stoppedReadErr
}

func (f *fakeStoppedSourceResolver) ImportStoppedSourceRelease(
	_ context.Context,
	input catalogapp.StoppedSourceReleaseInput,
) (catalogapp.RegisteredRelease, catalogapp.StoppedSourceReleaseReference, error) {
	f.imports++
	f.importInput = input
	return f.importedRelease, f.importedReference, f.importErr
}

func TestResolveStoppedSourceReleaseReusesExactSignedRelease(t *testing.T) {
	input, release, reference := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionSignedPublisher)
	authority := &fakeStoppedSourceResolver{release: release}

	resolution, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
	if err != nil {
		t.Fatalf("resolve signed release: %v", err)
	}
	if resolution.Release.AdmissionKind != catalogapp.ReleaseAdmissionSignedPublisher ||
		resolution.Reference != reference || authority.reads != 1 || authority.stoppedReads != 0 || authority.imports != 0 ||
		authority.readAppID != input.AppID || authority.readDigest != input.ExpectedAppReleaseDigest {
		t.Fatalf("resolution=%#v authority=%#v", resolution, authority)
	}
}

func TestResolveStoppedSourceReleaseReadsExactRetainedSourceReceipt(t *testing.T) {
	input, release, reference := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionStoppedSource)
	authority := &fakeStoppedSourceResolver{release: release, stoppedRelease: release}

	resolution, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
	if err != nil {
		t.Fatalf("resolve retained source release: %v", err)
	}
	if resolution.Release.AdmissionKind != catalogapp.ReleaseAdmissionStoppedSource ||
		resolution.Reference != reference || authority.reads != 1 || authority.stoppedReads != 1 ||
		authority.readReference != reference || authority.imports != 0 {
		t.Fatalf("resolution=%#v authority=%#v", resolution, authority)
	}
}

func TestResolveStoppedSourceReleaseImportsOnlyAfterNotFound(t *testing.T) {
	input, release, reference := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionStoppedSource)
	authority := &fakeStoppedSourceResolver{
		readErr: catalogapp.ErrNotFound, importedRelease: release, importedReference: reference,
	}

	resolution, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
	if err != nil {
		t.Fatalf("resolve missing stopped-source release: %v", err)
	}
	if resolution.Reference != reference || authority.reads != 1 || authority.stoppedReads != 0 || authority.imports != 1 {
		t.Fatalf("resolution=%#v authority=%#v", resolution, authority)
	}
	if authority.importInput.AppID != input.AppID ||
		authority.importInput.ExpectedAppReleaseDigest != input.ExpectedAppReleaseDigest ||
		authority.importInput.ExpectedPackageContentDigest != input.ExpectedPackageContentDigest ||
		authority.importInput.SourceRepository != input.SourceRepository ||
		authority.importInput.SourceCommit != input.SourceCommit ||
		!slices.Equal(authority.importInput.PackageBytes, input.PackageBytes) {
		t.Fatalf("import input = %#v, want exact source input %#v", authority.importInput, input)
	}
}

func TestResolveStoppedSourceReleaseNeverFallsBackAfterReadFailure(t *testing.T) {
	input, _, _ := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionStoppedSource)
	readErr := errors.New("catalog unavailable")
	authority := &fakeStoppedSourceResolver{readErr: readErr}

	_, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
	if !errors.Is(err, readErr) {
		t.Fatalf("resolve read failure = %v, want %v", err, readErr)
	}
	if authority.reads != 1 || authority.stoppedReads != 0 || authority.imports != 0 {
		t.Fatalf("read failure triggered fallback: %#v", authority)
	}
}

func TestResolveStoppedSourceReleaseRejectsExistingSignedPackageDriftWithoutFallback(t *testing.T) {
	input, exact, _ := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionSignedPublisher)
	_, drifted, _ := fixtureStoppedSourceResolutionWithBehavior(
		t,
		catalogapp.ReleaseAdmissionSignedPublisher,
		[]byte(`{"fields":["inactive"]}`),
	)
	bindReleaseCoordinates(&drifted, exact)
	authority := &fakeStoppedSourceResolver{release: drifted}

	_, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
	if !errors.Is(err, catalogapp.ErrConflict) {
		t.Fatalf("signed package drift error = %v, want conflict", err)
	}
	if authority.reads != 1 || authority.stoppedReads != 0 || authority.imports != 0 {
		t.Fatalf("signed package drift triggered fallback: %#v", authority)
	}
}

func TestResolveStoppedSourceReleaseRejectsRetainedSourcePackageDrift(t *testing.T) {
	input, exact, _ := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionStoppedSource)
	_, drifted, _ := fixtureStoppedSourceResolutionWithBehavior(
		t,
		catalogapp.ReleaseAdmissionStoppedSource,
		[]byte(`{"fields":["inactive"]}`),
	)
	bindReleaseCoordinates(&drifted, exact)
	authority := &fakeStoppedSourceResolver{release: exact, stoppedRelease: drifted}

	_, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
	if !errors.Is(err, catalogapp.ErrConflict) {
		t.Fatalf("retained source package drift error = %v, want conflict", err)
	}
	if authority.reads != 1 || authority.stoppedReads != 1 || authority.imports != 0 {
		t.Fatalf("retained source package drift triggered fallback: %#v", authority)
	}
}

func TestResolveStoppedSourceReleaseRejectsImportOutsideVerifiedAuthority(t *testing.T) {
	input, release, reference := fixtureStoppedSourceResolution(t, catalogapp.ReleaseAdmissionStoppedSource)
	tests := []struct {
		name      string
		release   catalogapp.RegisteredRelease
		reference catalogapp.StoppedSourceReleaseReference
	}{
		{
			name:    "wrong receipt",
			release: release,
			reference: func() catalogapp.StoppedSourceReleaseReference {
				other := reference
				other.SourceCommit = strings.Repeat("2", 40)
				return other
			}(),
		},
		{
			name: "signed admission",
			release: func() catalogapp.RegisteredRelease {
				signed := release
				signed.AdmissionKind = catalogapp.ReleaseAdmissionSignedPublisher
				return signed
			}(),
			reference: reference,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			authority := &fakeStoppedSourceResolver{
				readErr: catalogapp.ErrNotFound, importedRelease: test.release, importedReference: test.reference,
			}
			_, err := ResolveStoppedSourceRelease(context.Background(), authority, input)
			if !errors.Is(err, catalogapp.ErrConflict) {
				t.Fatalf("invalid import error = %v, want conflict", err)
			}
			if authority.imports != 1 {
				t.Fatalf("import calls = %d, want 1", authority.imports)
			}
		})
	}
}

type fakeStoppedSourceReconcileAuthority struct {
	*fakeAuthority
	stoppedRelease catalogapp.RegisteredRelease
	ordinaryReads  int
	stoppedReads   int
	ordinaryBegins int
	stoppedBegins  int
	beginRequest   catalogapp.BeginOperationRequest
	beginReference catalogapp.StoppedSourceReleaseReference
}

func (f *fakeStoppedSourceReconcileAuthority) ReadRelease(
	ctx context.Context,
	appID, digest string,
) (catalogapp.RegisteredRelease, error) {
	f.ordinaryReads++
	return f.fakeAuthority.ReadRelease(ctx, appID, digest)
}

func (f *fakeStoppedSourceReconcileAuthority) BeginOperation(
	ctx context.Context,
	request catalogapp.BeginOperationRequest,
) (catalogapp.Operation, error) {
	f.ordinaryBegins++
	f.beginRequest = request
	return f.fakeAuthority.BeginOperation(ctx, request)
}

func (f *fakeStoppedSourceReconcileAuthority) ReadStoppedSourceRelease(
	context.Context,
	catalogapp.StoppedSourceReleaseReference,
) (catalogapp.RegisteredRelease, error) {
	f.stoppedReads++
	if f.stoppedRelease.AppID != "" {
		return f.stoppedRelease, nil
	}
	return f.release, nil
}

func (f *fakeStoppedSourceReconcileAuthority) BeginStoppedSourceOperation(
	_ context.Context,
	request catalogapp.BeginOperationRequest,
	reference catalogapp.StoppedSourceReleaseReference,
) (catalogapp.Operation, error) {
	f.stoppedBegins++
	f.beginRequest, f.beginReference = request, reference
	return f.operation, nil
}

func TestStoppedSourceOperationAdapterSelectsOnlyTheAdmissionOwnedEntrypoint(t *testing.T) {
	request := catalogapp.BeginOperationRequest{
		Kind: catalogapp.OperationPrepare, OrgID: "org-test", AppID: "io.example.crm",
		TargetAppReleaseDigest: digestOf('a'), IdempotencyKey: "prepare-source",
	}
	for _, test := range []struct {
		name          string
		admission     catalogapp.ReleaseAdmissionKind
		ordinaryCalls int
		stoppedCalls  int
	}{
		{name: "signed", admission: catalogapp.ReleaseAdmissionSignedPublisher, ordinaryCalls: 1},
		{name: "stopped source", admission: catalogapp.ReleaseAdmissionStoppedSource, stoppedCalls: 1},
	} {
		t.Run(test.name, func(t *testing.T) {
			input, release, reference := fixtureStoppedSourceResolution(t, test.admission)
			_ = input
			authority := &fakeStoppedSourceReconcileAuthority{fakeAuthority: &fakeAuthority{
				release:   release,
				operation: catalogapp.Operation{OperationID: "operation-test"},
			}}
			adapter := stoppedSourceOperationAdapter{
				StoppedSourceReconcileAuthority: authority,
				resolution:                      StoppedSourceReleaseResolution{Release: release, Reference: reference},
			}

			if _, err := adapter.BeginOperation(context.Background(), request); err != nil {
				t.Fatalf("begin %s release: %v", test.name, err)
			}
			if authority.ordinaryBegins != test.ordinaryCalls || authority.stoppedBegins != test.stoppedCalls {
				t.Fatalf("begin calls ordinary=%d stopped=%d", authority.ordinaryBegins, authority.stoppedBegins)
			}
			if authority.beginRequest.IdempotencyKey != request.IdempotencyKey {
				t.Fatalf("begin request = %#v", authority.beginRequest)
			}
			if test.stoppedCalls == 1 && authority.beginReference != reference {
				t.Fatalf("stopped-source reference = %#v, want %#v", authority.beginReference, reference)
			}
		})
	}
}

func TestStoppedSourceOperationAdapterRereadsTheAdmissionOwnedAuthority(t *testing.T) {
	for _, test := range []struct {
		name          string
		admission     catalogapp.ReleaseAdmissionKind
		ordinaryReads int
		stoppedReads  int
	}{
		{name: "signed", admission: catalogapp.ReleaseAdmissionSignedPublisher, ordinaryReads: 1},
		{name: "stopped source", admission: catalogapp.ReleaseAdmissionStoppedSource, stoppedReads: 1},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, release, reference := fixtureStoppedSourceResolution(t, test.admission)
			authority := &fakeStoppedSourceReconcileAuthority{fakeAuthority: &fakeAuthority{release: release}}
			if test.admission == catalogapp.ReleaseAdmissionStoppedSource {
				authority.stoppedRelease = release
			}
			adapter := stoppedSourceOperationAdapter{
				StoppedSourceReconcileAuthority: authority,
				resolution:                      StoppedSourceReleaseResolution{Release: release, Reference: reference},
			}

			got, err := adapter.ReadRelease(context.Background(), release.AppID, release.AppReleaseDigest)
			if err != nil {
				t.Fatalf("read %s release: %v", test.name, err)
			}
			if !sameResolvedRelease(got, release) ||
				authority.ordinaryReads != test.ordinaryReads || authority.stoppedReads != test.stoppedReads {
				t.Fatalf("release=%#v ordinaryReads=%d stoppedReads=%d", got, authority.ordinaryReads, authority.stoppedReads)
			}
		})
	}
}

func TestStoppedSourceOperationAdapterRejectsPostResolutionPackageDrift(t *testing.T) {
	for _, admission := range []catalogapp.ReleaseAdmissionKind{
		catalogapp.ReleaseAdmissionSignedPublisher,
		catalogapp.ReleaseAdmissionStoppedSource,
	} {
		t.Run(string(admission), func(t *testing.T) {
			_, exact, reference := fixtureStoppedSourceResolution(t, admission)
			_, drifted, _ := fixtureStoppedSourceResolutionWithBehavior(
				t,
				admission,
				[]byte(`{"fields":["inactive"]}`),
			)
			bindReleaseCoordinates(&drifted, exact)
			authority := &fakeStoppedSourceReconcileAuthority{fakeAuthority: &fakeAuthority{release: drifted}}
			if admission == catalogapp.ReleaseAdmissionStoppedSource {
				authority.stoppedRelease = drifted
			}
			adapter := stoppedSourceOperationAdapter{
				StoppedSourceReconcileAuthority: authority,
				resolution:                      StoppedSourceReleaseResolution{Release: exact, Reference: reference},
			}

			_, err := adapter.ReadRelease(context.Background(), exact.AppID, exact.AppReleaseDigest)
			if !errors.Is(err, catalogapp.ErrConflict) {
				t.Fatalf("post-resolution package drift error = %v, want conflict", err)
			}
		})
	}
}

func fixtureStoppedSourceResolution(
	t *testing.T,
	admission catalogapp.ReleaseAdmissionKind,
) (catalogapp.StoppedSourceReleaseInput, catalogapp.RegisteredRelease, catalogapp.StoppedSourceReleaseReference) {
	return fixtureStoppedSourceResolutionWithBehavior(
		t,
		admission,
		[]byte(`{"fields":["active"]}`),
	)
}

func fixtureStoppedSourceResolutionWithBehavior(
	t *testing.T,
	admission catalogapp.ReleaseAdmissionKind,
	behavior []byte,
) (catalogapp.StoppedSourceReleaseInput, catalogapp.RegisteredRelease, catalogapp.StoppedSourceReleaseReference) {
	t.Helper()
	contentDigest, err := appauthority.ComponentDigest("metadata.object.v1", 1, behavior)
	if err != nil {
		t.Fatal(err)
	}
	component := appauthority.Component{
		ComponentKind: "metadata.object.v1", ComponentFormatVersion: 1, NamespaceID: "io.example.crm",
		BusinessKey: "WorkItem", DefinitionVersion: "1", LocalDependencies: []string{},
		ExternalComponentRefs: []appauthority.ExternalComponentRef{}, OverridePolicy: appauthority.OverridePolicyReplace,
		RemovalPolicy: appauthority.RemovalPolicyBlockIfReferencedOrOverridden, ContentDigest: contentDigest,
	}
	component.ComponentID = appauthority.ComponentID(component.NamespaceID, component.ComponentKind, component.BusinessKey)
	bundle := appauthority.MetadataBundle{SchemaVersion: appauthority.MetadataBundleSchemaVersion, Components: []appauthority.Component{component}}
	manifest := appauthority.AppManifest{
		SchemaVersion: appauthority.AppManifestSchemaVersion, AppID: "io.example.crm", PublisherID: "io.example",
		SemanticVersion: "1.0.0", MetadataBundleDigest: digestOf('0'), Dependencies: []appauthority.Dependency{},
		OwnedNamespaces:            []string{"io.example.crm"},
		RequiredProtocols:          []appauthority.ProtocolRequirement{{ProtocolID: "nexuscrm.runtime.v1", Major: 1, MinimumMinor: 1}},
		RequiredEngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
		ProvidedCapabilities: []appauthority.ProvidedCapability{{
			CapabilityID: "io.example.crm.ui.access.v1", Kind: appauthority.CapabilityKindServiceHandler,
			HandlerRef:                  "io.example.crm.handler.ui.access.v1",
			RequiredRuntimeCapabilities: []string{"runtime.records.evaluate-access.v1"},
		}},
		FrontendCompatibility: appauthority.FrontendCompatibility{Mode: appauthority.FrontendModeNone},
	}
	manifest.MetadataBundleDigest, err = appauthority.MetadataBundleDigest(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	pkg := appauthority.Package{
		Manifest: manifest, Bundle: bundle,
		ComponentBlobs: []appauthority.ComponentBlob{{ComponentID: component.ComponentID, CanonicalBehavior: behavior}},
	}
	artifact, packageBytes, err := releaseartifact.BuildPackage(pkg)
	if err != nil {
		t.Fatal(err)
	}
	packageSum := sha256.Sum256(packageBytes)
	compatibility := releaseartifact.HostCompatibility{
		Protocols:          []releaseartifact.SupportedProtocol{{ProtocolID: "nexuscrm.runtime.v1", Major: 1, Minor: 1}},
		EngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
		ComponentKinds:     []releaseartifact.SupportedComponentKind{{ComponentKind: "metadata.object.v1", ComponentFormatVersion: 1}},
	}
	input := catalogapp.StoppedSourceReleaseInput{
		AppID: "io.example.crm", ExpectedAppReleaseDigest: artifact.AppReleaseDigest,
		ExpectedPackageContentDigest: "sha256:" + stringHex(packageSum[:]),
		SourceRepository:             "pingcap-inc/nexuscrm-app-packs", SourceCommit: strings.Repeat("1", 40),
		PackageBytes: packageBytes, Compatibility: compatibility,
	}
	candidate, reference, err := catalogapp.VerifyStoppedSourceReleaseCandidate(input)
	if err != nil {
		t.Fatal(err)
	}
	release := catalogapp.RegisteredRelease{
		AppID: candidate.AppID, PublisherID: candidate.PublisherID, SemanticVersion: candidate.SemanticVersion,
		AppReleaseDigest: candidate.AppReleaseDigest, MetadataBundleDigest: candidate.MetadataBundleDigest,
		AdmissionKind: admission, Package: candidate.Package,
	}
	return input, release, reference
}

func bindReleaseCoordinates(target *catalogapp.RegisteredRelease, source catalogapp.RegisteredRelease) {
	target.AppID = source.AppID
	target.PublisherID = source.PublisherID
	target.SemanticVersion = source.SemanticVersion
	target.AppReleaseDigest = source.AppReleaseDigest
	target.MetadataBundleDigest = source.MetadataBundleDigest
	target.AdmissionKind = source.AdmissionKind
}

func stringHex(raw []byte) string {
	const alphabet = "0123456789abcdef"
	encoded := make([]byte, len(raw)*2)
	for i, value := range raw {
		encoded[i*2] = alphabet[value>>4]
		encoded[i*2+1] = alphabet[value&0x0f]
	}
	return string(encoded)
}

func TestOrdinaryConfigParserRejectsStoppedSourceWrapper(t *testing.T) {
	if _, err := ParseConfig([]byte(fixtureStoppedSourceConfigJSON(t))); err == nil {
		t.Fatal("ordinary Catalog reconcile accepted the stopped-source wrapper")
	}
}

func fixtureStoppedSourceConfigJSON(t *testing.T) string {
	t.Helper()
	reconcile := fixtureConfigJSON()
	start := strings.Index(reconcile, `"releaseImport":`)
	end := strings.Index(reconcile[start:], "\n\"artifactStoreRoot\"")
	if start < 0 || end < 0 {
		t.Fatal("fixture releaseImport block not found")
	}
	reconcile = reconcile[:start] + reconcile[start+end+1:]
	reconcile = strings.Replace(reconcile, ConfigSchemaV1, ConfigSchemaV4, 1)
	reconcile = strings.Replace(
		reconcile,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`",`,
		`"expectedAppReleaseDigest":"`+digestOf('a')+`","metadataSourceDigest":"`+digestOf('8')+`",`,
		1,
	)
	reconcile = strings.Replace(
		reconcile,
		`"workloadId":"workload-test",`,
		`"workloadId":"workload-test","authorizedWorkloadIds":["workload-test"],`,
		1,
	)
	reconcile = strings.Replace(
		reconcile,
		`"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,
	)
	return `{"schema":"` + StoppedSourceConfigSchemaV1 + `","sourceRelease":{` +
		`"packagePath":"/artifacts/app-package.json",` +
		`"appId":"io.nexuscrm.sfcrm",` +
		`"expectedAppReleaseDigest":"` + digestOf('a') + `",` +
		`"expectedPackageContentDigest":"` + digestOf('6') + `",` +
		`"sourceRepository":"pingcap-inc/nexuscrm-app-packs",` +
		`"sourceCommit":"` + strings.Repeat("1", 40) + `"},"reconcile":` + reconcile + `}`
}
