package serving

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"reflect"
	"strings"
	"testing"

	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

func TestPreparedEpochCanonicalGoldenAndDefensiveCopies(t *testing.T) {
	receipt := []byte("l2-verification-receipt-v1\x00generation-17")
	l2, err := NewL2Snapshot(
		"connector-config-generation-17",
		"sha256:"+strings.Repeat("d", 64),
		receipt,
	)
	if err != nil {
		t.Fatal(err)
	}
	view := canonicalTestBindingView()
	epoch, err := NewPreparedEpoch("epoch-canonical-1", view, l2)
	if err != nil {
		t.Fatal(err)
	}

	if len(view.Bindings[0].PackPayloadDigest) != 85 {
		t.Fatalf("PackPayloadDigest length = %d, want exact catalog length 85", len(view.Bindings[0].PackPayloadDigest))
	}
	goldens := map[string][2]string{
		"serving scope": {
			epoch.servingScopeDigest,
			"b957081f8f22f64269e5b8b67b5ce24c114d07be610f3939837205a85fb0f27c",
		},
		"binding identity": {
			epoch.bindings[0].identityDigest,
			"c9d08056e814482028dda91395ccf06df31d1cc41d5e6a4fb4319fdb8c31d8a5",
		},
		"binding row ID": {
			epoch.bindings[0].id,
			"5278ed47a285d81177f7457ea7b2e3e7acfb582f55bd7635c5f97854f7f89637",
		},
		"credential refs": {
			hex.EncodeToString(epoch.bindings[0].credentialRefs),
			"6e6578757363726d2e636f6e6e6563746f722d73657276696e672d63726564656e7469616c2d726566732e7631000000000000000002000000000000000c63726564656e7469616c2d61000000000000000c63726564656e7469616c2d62",
		},
		"L2 receipt": {
			epoch.l2.verificationReceiptDigest,
			"sha256:e5dda736e3db29ad842348b69c39b72fb11376108639a426b2a7e7f50f548dd8",
		},
		"epoch": {
			epoch.epochDigest,
			"sha256:e8b3d165f1d86c660c3567f18b0d925f2284e45d9cd9ae5473fdfddff02842f2",
		},
	}
	for name, values := range goldens {
		if values[0] != values[1] {
			t.Errorf("%s golden = %q, want %q", name, values[0], values[1])
		}
	}
	if !rawSHA256Pattern.MatchString(epoch.servingScopeDigest) ||
		!rawSHA256Pattern.MatchString(epoch.bindings[0].identityDigest) {
		t.Fatalf("scope/binding digests are not raw lowercase SHA-256: %q/%q", epoch.servingScopeDigest, epoch.bindings[0].identityDigest)
	}
	if !prefixedSHA256Pattern.MatchString(epoch.epochDigest) ||
		!prefixedSHA256Pattern.MatchString(epoch.l2.verificationReceiptDigest) {
		t.Fatalf("epoch/receipt digests are not prefixed SHA-256: %q/%q", epoch.epochDigest, epoch.l2.verificationReceiptDigest)
	}

	second, err := NewPreparedEpoch("epoch-canonical-1", view, l2)
	if err != nil || !epoch.equal(second) {
		t.Fatalf("deterministic rebuild = equal:%t err:%v, want true/nil", epoch.equal(second), err)
	}

	// The Catalog view is transient admission input. Mutating it after
	// construction cannot alter the retained immutable reference projection.
	expected, err := NewPreparedEpoch(
		"epoch-canonical-1",
		canonicalTestBindingView(),
		canonicalTestL2Snapshot(t),
	)
	if err != nil {
		t.Fatal(err)
	}
	receipt[0] = 'X'
	view.Bindings[0].CredentialRefs[0] = "mutated-input"
	view.Bindings[1].RuntimeCommand.DefinitionRef = "mutated-input"
	gotL2 := epoch.L2Snapshot()
	gotL2.verificationReceipt[0] = 'Y'
	credentialRefs, err := decodeCredentialRefs(epoch.bindings[0].credentialRefs)
	if err != nil {
		t.Fatal(err)
	}
	if string(epoch.l2.verificationReceipt) != "l2-verification-receipt-v1\x00generation-17" ||
		!epoch.equal(expected) || credentialRefs[0] != "credential-a" ||
		*epoch.bindings[1].runtimeDefinitionRef != "Companion_Flow" {
		t.Fatal("caller mutation escaped a defensive-copy boundary")
	}
}

func TestPreparedEpochCannotBecomeCatalogDefinitionReadPath(t *testing.T) {
	epochType := reflect.TypeOf(PreparedEpoch{})
	viewType := reflect.TypeOf(catalogv1.IntegrationBindingView{})
	bindingType := reflect.TypeOf(catalogv1.IntegrationBinding{})
	for i := 0; i < epochType.NumField(); i++ {
		field := epochType.Field(i)
		if isCatalogDefinitionType(field.Type, viewType, bindingType) {
			t.Fatalf("PreparedEpoch field %q retains a Catalog definition-shaped value", field.Name)
		}
	}
	for _, methodSet := range []reflect.Type{epochType, reflect.PointerTo(epochType)} {
		for i := 0; i < methodSet.NumMethod(); i++ {
			method := methodSet.Method(i)
			for output := 0; output < method.Type.NumOut(); output++ {
				if isCatalogDefinitionType(method.Type.Out(output), viewType, bindingType) {
					t.Fatalf("PreparedEpoch method %q exposes a Catalog definition-shaped read path", method.Name)
				}
			}
		}
	}
}

func isCatalogDefinitionType(value, view, binding reflect.Type) bool {
	return isCatalogDefinitionTypeSeen(value, view, binding, make(map[reflect.Type]bool))
}

func isCatalogDefinitionTypeSeen(value, view, binding reflect.Type, seen map[reflect.Type]bool) bool {
	if value == view || value == binding {
		return true
	}
	switch value.Kind() {
	case reflect.Array, reflect.Pointer, reflect.Slice:
		return isCatalogDefinitionTypeSeen(value.Elem(), view, binding, seen)
	case reflect.Map:
		return isCatalogDefinitionTypeSeen(value.Key(), view, binding, seen) ||
			isCatalogDefinitionTypeSeen(value.Elem(), view, binding, seen)
	case reflect.Struct:
		if seen[value] {
			return false
		}
		seen[value] = true
		for i := 0; i < value.NumField(); i++ {
			if isCatalogDefinitionTypeSeen(value.Field(i).Type, view, binding, seen) {
				return true
			}
		}
		return false
	default:
		return false
	}
}

func TestEpochDigestCoversEveryCandidateAxis(t *testing.T) {
	view := canonicalTestBindingView()
	l2 := canonicalTestL2Snapshot(t)
	base, err := NewPreparedEpoch("epoch-axis", view, l2)
	if err != nil {
		t.Fatal(err)
	}
	tests := candidateIdentityAxisMutations()

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			epochID := "epoch-axis"
			mutatedView := copyIntegrationBindingView(view)
			mutatedL2 := l2.copy()
			test.mutate(&epochID, &mutatedView, &mutatedL2)
			mutatedView.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(mutatedView)
			candidate, err := NewPreparedEpoch(epochID, mutatedView, mutatedL2)
			if err != nil {
				t.Fatalf("mutated candidate must remain valid: %v", err)
			}
			if candidate.epochDigest == base.epochDigest {
				t.Fatalf("epoch digest did not cover %s", test.name)
			}
		})
	}
}

type candidateIdentityAxisMutation struct {
	name           string
	changesEpochID bool
	mutate         func(*string, *catalogv1.IntegrationBindingView, *L2Snapshot)
}

func candidateIdentityAxisMutations() []candidateIdentityAxisMutation {
	return []candidateIdentityAxisMutation{
		{name: "epoch ID", changesEpochID: true, mutate: func(id *string, _ *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			*id = "epoch-axis-2"
		}},
		{name: "organization", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.OrgID = "org-serving-2"
		}},
		{name: "application installation", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.AppInstallationID = "01900000-0000-7000-8000-000000000002"
		}},
		{name: "authorized workload", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.AuthorizedWorkloadID = "integration-serving-2"
		}},
		{name: "catalog revision receipt tuple", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Lineage.CatalogRevision++
			view.Receipts.CatalogRevision++
		}},
		{name: "schema generation receipt tuple", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Lineage.SchemaGeneration++
			view.Receipts.SchemaGeneration++
		}},
		{name: "engine generation receipt tuple", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Lineage.EngineGeneration++
			view.Receipts.EngineGeneration++
		}},
		{name: "source snapshot", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Lineage.SourceSnapshotDigest = "sha256:" + strings.Repeat("c", 64)
		}},
		{name: "security epoch", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Receipts.SecurityEpoch++
		}},
		{name: "binding kind", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].Kind = catalogv1.BindingConnectorFieldMapping
		}},
		{name: "binding owner pack", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].OwnerPack = "connector-pack-2"
		}},
		{name: "binding installation", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].PackInstallationID = "install-2"
		}},
		{name: "binding pack payload digest", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].PackPayloadDigest = "nexus-pack-sha256-v1:" + strings.Repeat("b", 64)
		}},
		{name: "binding business key", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].BusinessKey = "connector-pack/callout-2"
		}},
		{name: "binding definition version", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].DefinitionVersion++
		}},
		{name: "credential blob", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[0].CredentialRefs = []string{"credential-a", "credential-c"}
		}},
		{name: "runtime presence", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[1].RuntimeCommand = nil
		}},
		{name: "runtime command kind", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[1].RuntimeCommand.CommandKind = "runtime.flow.execute.v2"
		}},
		{name: "runtime payload format", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[1].RuntimeCommand.PayloadFormatVersion++
		}},
		{name: "runtime definition ref", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[1].RuntimeCommand.DefinitionRef = "Companion_Flow_2"
		}},
		{name: "runtime definition version", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings[1].RuntimeCommand.DefinitionVersion = "flow-version-8"
		}},
		{name: "binding count", mutate: func(_ *string, view *catalogv1.IntegrationBindingView, _ *L2Snapshot) {
			view.Bindings = view.Bindings[:1]
		}},
		{name: "L2 generation", mutate: func(_ *string, _ *catalogv1.IntegrationBindingView, l2 *L2Snapshot) {
			l2.configurationGenerationRef = "connector-config-generation-18"
		}},
		{name: "L2 configuration digest", mutate: func(_ *string, _ *catalogv1.IntegrationBindingView, l2 *L2Snapshot) {
			l2.configurationDigest = "sha256:" + strings.Repeat("e", 64)
		}},
		{name: "L2 receipt and digest", mutate: func(_ *string, _ *catalogv1.IntegrationBindingView, l2 *L2Snapshot) {
			l2.verificationReceipt = []byte("l2-verification-receipt-v1\x00generation-18")
			l2.verificationReceiptDigest = computeL2VerificationReceiptDigest(l2.verificationReceipt)
		}},
	}
}

func TestBindingRowIDCoversEpochOrdinalAndBindingIdentity(t *testing.T) {
	view := canonicalTestBindingView()
	first, err := NewPreparedEpoch("epoch-row", view, canonicalTestL2Snapshot(t))
	if err != nil {
		t.Fatal(err)
	}
	base := first.bindings[0]
	if base.id == computeBindingRowID("epoch-row-2", base.ordinal, base.identityDigest) {
		t.Fatal("binding row ID omitted epoch ID")
	}
	if base.id == computeBindingRowID("epoch-row", base.ordinal+1, base.identityDigest) {
		t.Fatal("binding row ID omitted ordinal")
	}
	if base.id == computeBindingRowID("epoch-row", base.ordinal, strings.Repeat("0", 64)) {
		t.Fatal("binding row ID omitted binding identity digest")
	}
}

func TestCredentialRefsCanonicalRoundTripAndDecoding(t *testing.T) {
	values := []string{"Credential-A", "credential-a", "credential-b"}
	encoded, err := encodeCredentialRefs(values)
	if err != nil {
		t.Fatal(err)
	}
	prefix := append([]byte(credentialRefsDomain), 0)
	if !bytes.HasPrefix(encoded, prefix) {
		t.Fatalf("credential blob prefix = %q, want domain plus zero delimiter", encoded[:len(prefix)])
	}
	decoded, err := decodeCredentialRefs(encoded)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Join(decoded, "\x00") != strings.Join(values, "\x00") {
		t.Fatalf("decoded refs = %#v, want %#v", decoded, values)
	}
	reencoded, err := encodeCredentialRefs(decoded)
	if err != nil || !bytes.Equal(reencoded, encoded) {
		t.Fatalf("canonical re-encode = equal:%t err:%v", bytes.Equal(reencoded, encoded), err)
	}

	empty, err := encodeCredentialRefs(nil)
	if err != nil {
		t.Fatal(err)
	}
	if decoded, err := decodeCredentialRefs(empty); err != nil || len(decoded) != 0 {
		t.Fatalf("empty canonical refs = %#v/%v", decoded, err)
	}
	left, _ := encodeCredentialRefs([]string{"ab", "c"})
	right, _ := encodeCredentialRefs([]string{"a", "bc"})
	if bytes.Equal(left, right) {
		t.Fatal("length framing admitted a structural collision")
	}

	malformed := map[string][]byte{
		"wrong domain": append([]byte("wrong-domain\x00"), encoded[len(prefix):]...),
		"truncated":    encoded[:len(encoded)-1],
		"trailing":     append(append([]byte(nil), encoded...), 0),
		"excess count": binary.BigEndian.AppendUint64(canonicalPrefix(credentialRefsDomain), catalogv1.MaxCredentialRefCount+1),
		"unsorted":     credentialBlobWithoutValidation([][]byte{[]byte("b"), []byte("a")}),
		"duplicate":    credentialBlobWithoutValidation([][]byte{[]byte("a"), []byte("a")}),
		"whitespace":   credentialBlobWithoutValidation([][]byte{[]byte(" credential")}),
		"NUL":          credentialBlobWithoutValidation([][]byte{[]byte("cred\x00ential")}),
		"control":      credentialBlobWithoutValidation([][]byte{[]byte("cred\nential")}),
		"invalid UTF8": credentialBlobWithoutValidation([][]byte{{0xff}}),
		"too long":     credentialBlobWithoutValidation([][]byte{[]byte(strings.Repeat("x", catalogv1.MaxIdentifierBytes+1))}),
	}
	for name, value := range malformed {
		t.Run(name, func(t *testing.T) {
			if decoded, err := decodeCredentialRefs(value); err == nil {
				t.Fatalf("decodeCredentialRefs() = %#v, want rejection", decoded)
			}
		})
	}
}

func TestCandidateRejectsNonCanonicalIdentityAndDigestShape(t *testing.T) {
	view := canonicalTestBindingView()
	l2 := canonicalTestL2Snapshot(t)
	invalidIdentifiers := []string{
		"",
		" leading",
		"trailing ",
		"embedded\x00nul",
		"embedded\ncontrol",
		"embedded\u0085control",
		string([]byte{0xff}),
		strings.Repeat("x", catalogv1.MaxIdentifierBytes+1),
	}
	for _, value := range invalidIdentifiers {
		if _, err := NewPreparedEpoch(value, view, l2); !errors.Is(err, ErrInvalidCandidate) {
			t.Fatalf("epoch ID %q error = %v, want ErrInvalidCandidate", value, err)
		}
		if _, err := NewL2Snapshot(value, "sha256:"+strings.Repeat("d", 64), []byte("receipt")); !errors.Is(err, ErrInvalidCandidate) {
			t.Fatalf("L2 ref %q error = %v, want ErrInvalidCandidate", value, err)
		}
	}
	if _, err := NewPreparedEpoch(strings.Repeat("x", catalogv1.MaxIdentifierBytes), view, l2); err != nil {
		t.Fatalf("exact %d-byte epoch ID rejected: %v", catalogv1.MaxIdentifierBytes, err)
	}
	for _, digest := range []string{
		"",
		strings.Repeat("d", 64),
		"sha256:" + strings.Repeat("D", 64),
		"sha256:" + strings.Repeat("d", 63),
		" sha256:" + strings.Repeat("d", 64),
	} {
		if _, err := NewL2Snapshot("generation", digest, []byte("receipt")); !errors.Is(err, ErrInvalidCandidate) {
			t.Fatalf("L2 digest %q error = %v, want ErrInvalidCandidate", digest, err)
		}
	}
	if _, err := NewL2Snapshot("generation", "sha256:"+strings.Repeat("d", 64), nil); !errors.Is(err, ErrInvalidCandidate) {
		t.Fatalf("empty L2 receipt error = %v, want ErrInvalidCandidate", err)
	}
	maxIdentifier := strings.Repeat("x", catalogv1.MaxIdentifierBytes)
	maxL2, err := NewL2Snapshot(maxIdentifier, "sha256:"+strings.Repeat("d", 64), []byte("receipt"))
	if err != nil {
		t.Fatalf("maximum-length L2 generation rejected: %v", err)
	}
	if _, err := NewPreparedEpoch(maxIdentifier, view, maxL2); err != nil {
		t.Fatalf("maximum-length epoch ID rejected: %v", err)
	}
	// Receipts are opaque bytes, not identifiers; arbitrary binary bytes remain
	// representable and are bound exactly by their canonical digest.
	if _, err := NewL2Snapshot("generation", "sha256:"+strings.Repeat("d", 64), []byte{0, 0xff}); err != nil {
		t.Fatalf("opaque binary L2 receipt rejected: %v", err)
	}

	invalidView := copyIntegrationBindingView(view)
	invalidView.Lineage.ViewDigest = "sha256:" + strings.Repeat("0", 64)
	if _, err := NewPreparedEpoch("epoch", invalidView, l2); !errors.Is(err, ErrInvalidCandidate) {
		t.Fatalf("disagreeing Catalog digest error = %v, want ErrInvalidCandidate", err)
	}
	invalidView = copyIntegrationBindingView(view)
	invalidView.Receipts.SecurityEpoch = 10
	invalidView.AuthorizedWorkloadID = " invalid"
	invalidView.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(invalidView)
	if _, err := NewPreparedEpoch("epoch", invalidView, l2); !errors.Is(err, ErrInvalidCandidate) {
		t.Fatalf("invalid Catalog view error = %v, want ErrInvalidCandidate", err)
	}
}

func TestNewL2SnapshotEnforcesConnectorPersistedReceiptLimit(t *testing.T) {
	digest := "sha256:" + strings.Repeat("d", 64)
	if maxPersistedL2VerificationReceiptBytes != 1024*1024 {
		t.Fatalf("Connector persisted receipt limit = %d, want exactly one MiB", maxPersistedL2VerificationReceiptBytes)
	}
	if maxPersistedL2VerificationReceiptBytes != catalogv1.MaxSemanticResponseBytes {
		t.Fatalf(
			"Connector persisted receipt limit = %d, want Catalog semantic budget %d",
			maxPersistedL2VerificationReceiptBytes,
			catalogv1.MaxSemanticResponseBytes,
		)
	}
	if err := validateL2SnapshotShape("generation", digest, maxPersistedL2VerificationReceiptBytes); err != nil {
		t.Fatalf("exact Connector persisted receipt limit rejected: %v", err)
	}
	if err := validateL2SnapshotShape("generation", digest, maxPersistedL2VerificationReceiptBytes+1); err == nil ||
		!strings.Contains(err.Error(), "connector-owned persisted limit of 1048576 bytes") {
		t.Fatalf("receipt beyond Connector persisted limit error = %v, want explicit Connector-owned 1048576-byte boundary", err)
	}
	oversized := make([]byte, maxPersistedL2VerificationReceiptBytes+1)
	if _, err := NewL2Snapshot("generation", digest, oversized); !errors.Is(err, ErrInvalidCandidate) ||
		!strings.Contains(err.Error(), "connector-owned persisted limit of 1048576 bytes") {
		t.Fatalf("oversized receipt error = %v, want ErrInvalidCandidate and explicit Connector boundary", err)
	}
}

func TestValidateStoredAndExactEquality(t *testing.T) {
	base, err := NewPreparedEpoch("epoch-stored", canonicalTestBindingView(), canonicalTestL2Snapshot(t))
	if err != nil {
		t.Fatal(err)
	}
	if err := base.validateStored(); err != nil {
		t.Fatalf("validateStored(valid): %v", err)
	}
	copy := base.copy()
	if !base.equal(copy) {
		t.Fatal("deep copy is not exactly equal")
	}
	copy.bindings[0].credentialRefs[0] ^= 1
	if base.equal(copy) {
		t.Fatal("binding-byte mutation remained equal")
	}
	if bytes.Equal(base.bindings[0].credentialRefs, copy.bindings[0].credentialRefs) {
		t.Fatal("PreparedEpoch.copy shared binding bytes")
	}

	tests := []struct {
		name   string
		mutate func(*PreparedEpoch)
	}{
		{name: "state", mutate: func(epoch *PreparedEpoch) { epoch.state = "verified" }},
		{name: "active scope", mutate: func(epoch *PreparedEpoch) { epoch.activeScopeDigest = stringPointer(epoch.servingScopeDigest) }},
		{name: "epoch digest", mutate: func(epoch *PreparedEpoch) { epoch.epochDigest = "sha256:" + strings.Repeat("0", 64) }},
		{name: "scope digest", mutate: func(epoch *PreparedEpoch) { epoch.servingScopeDigest = strings.Repeat("0", 64) }},
		{name: "binding row ID", mutate: func(epoch *PreparedEpoch) { epoch.bindings[0].id = strings.Repeat("0", 64) }},
		{name: "binding ordinal", mutate: func(epoch *PreparedEpoch) { epoch.bindings[0].ordinal++ }},
		{name: "binding identity", mutate: func(epoch *PreparedEpoch) { epoch.bindings[0].identityDigest = strings.Repeat("0", 64) }},
		{name: "credential blob", mutate: func(epoch *PreparedEpoch) { epoch.bindings[0].credentialRefs = []byte("not-canonical") }},
		{name: "missing binding", mutate: func(epoch *PreparedEpoch) { epoch.bindings = epoch.bindings[:1] }},
		{name: "partial runtime tuple", mutate: func(epoch *PreparedEpoch) { epoch.bindings[1].runtimeDefinitionRef = nil }},
		{name: "L2 receipt digest", mutate: func(epoch *PreparedEpoch) { epoch.l2.verificationReceiptDigest = "sha256:" + strings.Repeat("0", 64) }},
		{name: "L2 receipt beyond Connector persisted limit", mutate: func(epoch *PreparedEpoch) {
			epoch.l2.verificationReceipt = make([]byte, maxPersistedL2VerificationReceiptBytes+1)
			epoch.l2.verificationReceiptDigest = computeL2VerificationReceiptDigest(epoch.l2.verificationReceipt)
		}},
		{name: "Catalog binding drift", mutate: func(epoch *PreparedEpoch) { epoch.bindings[0].definitionVersion++ }},
		{name: "receipt protocol", mutate: func(epoch *PreparedEpoch) { epoch.catalog.receipts.ProtocolVersion = "1.1" }},
		{name: "receipt capability", mutate: func(epoch *PreparedEpoch) { epoch.catalog.receipts.CapabilityVersion = "wrong-capability" }},
		{name: "receipt lineage tuple", mutate: func(epoch *PreparedEpoch) { epoch.catalog.receipts.CatalogRevision++ }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := base.copy()
			test.mutate(&candidate)
			if err := candidate.validateStored(); !errors.Is(err, ErrStoredCandidateInvalid) {
				t.Fatalf("validateStored() error = %v, want ErrStoredCandidateInvalid", err)
			}
		})
	}
}

func canonicalTestBindingView() catalogv1.IntegrationBindingView {
	view := catalogv1.IntegrationBindingView{
		OrgID:                "org-serving",
		AppInstallationID:    "01900000-0000-7000-8000-000000000001",
		AuthorizedWorkloadID: "integration-serving",
		Lineage: catalogv1.ViewLineage{
			CatalogRevision:      41,
			SchemaGeneration:     7,
			EngineGeneration:     8,
			SourceSnapshotDigest: "sha256:" + strings.Repeat("b", 64),
		},
		Bindings: []catalogv1.IntegrationBinding{
			{
				Kind:               catalogv1.BindingConnectorCalloutProfile,
				OwnerPack:          "connector-pack",
				PackInstallationID: "install-1",
				PackPayloadDigest:  "nexus-pack-sha256-v1:" + strings.Repeat("a", 64),
				BusinessKey:        "connector-pack/callout",
				DefinitionVersion:  3,
				CredentialRefs:     []string{"credential-a", "credential-b"},
			},
			{
				Kind:               catalogv1.BindingConnectorInboundRoute,
				OwnerPack:          "connector-pack",
				PackInstallationID: "install-1",
				PackPayloadDigest:  "nexus-pack-sha256-v1:" + strings.Repeat("a", 64),
				BusinessKey:        "connector-pack/contact.created",
				DefinitionVersion:  7,
				CredentialRefs:     []string{"credential-a"},
				RuntimeCommand: &catalogv1.RuntimeCommandBinding{
					CommandKind:          "runtime.flow.execute",
					PayloadFormatVersion: 1,
					DefinitionRef:        "Companion_Flow",
					DefinitionVersion:    "flow-version-7",
				},
			},
		},
		Receipts: catalogv1.Receipts{
			ProtocolVersion:   "1.0",
			CapabilityVersion: catalogv1.IntegrationBindingCapabilityName,
			CatalogRevision:   41,
			SchemaGeneration:  7,
			EngineGeneration:  8,
			SecurityEpoch:     9,
		},
	}
	view.Lineage.ViewDigest = catalogv1.ComputeIntegrationBindingViewDigest(view)
	return view
}

func copyIntegrationBindingView(view catalogv1.IntegrationBindingView) catalogv1.IntegrationBindingView {
	if view.Bindings == nil {
		return view
	}
	view.Bindings = append([]catalogv1.IntegrationBinding(nil), view.Bindings...)
	for i := range view.Bindings {
		view.Bindings[i].CredentialRefs = append([]string(nil), view.Bindings[i].CredentialRefs...)
		if view.Bindings[i].RuntimeCommand != nil {
			runtimeCommand := *view.Bindings[i].RuntimeCommand
			view.Bindings[i].RuntimeCommand = &runtimeCommand
		}
	}
	return view
}

func canonicalTestL2Snapshot(t *testing.T) L2Snapshot {
	t.Helper()
	snapshot, err := NewL2Snapshot(
		"connector-config-generation-17",
		"sha256:"+strings.Repeat("d", 64),
		[]byte("l2-verification-receipt-v1\x00generation-17"),
	)
	if err != nil {
		t.Fatal(err)
	}
	return snapshot
}

func credentialBlobWithoutValidation(values [][]byte) []byte {
	encoded := canonicalPrefix(credentialRefsDomain)
	encoded = binary.BigEndian.AppendUint64(encoded, uint64(len(values)))
	for _, value := range values {
		encoded = appendCanonicalBytes(encoded, value)
	}
	return encoded
}
