package layoutactivationsetv1

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"math"
	"reflect"
	"strings"
	"testing"

	"github.com/pingcap-inc/nexuscrm-contract/pkg/catalogpublicationv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/layoutroutingsecurityv1"
)

func TestActivationSetValueSurfaceIsExact(t *testing.T) {
	contractAssertExactFields(t, reflect.TypeOf(Entry{}), []string{
		"BundleKey",
		"LayoutBundleRevisionID",
		"LayoutValidationID",
		"ResolvedLayoutViewDigest",
	})
	contractAssertExactFields(t, reflect.TypeOf(Set{}), []string{
		"CanonicalizationVersion",
		"OrgID",
		"CatalogSchema",
		"RoutingSecurity",
		"RequiredBundleKeySetDigest",
		"PreparationProvenanceDigest",
		"Entries",
	})
	if reflect.TypeOf(LayoutBundleRevisionID("")).Kind() != reflect.String {
		t.Fatal("LayoutBundleRevisionID must retain exact string bytes")
	}
	if reflect.TypeOf(LayoutValidationID("")).Kind() != reflect.String {
		t.Fatal("LayoutValidationID must retain exact string bytes")
	}
	if CanonicalizationVersion != "nexuscrm.layout-activation-set-canonical/v1" {
		t.Fatalf("CanonicalizationVersion = %q", CanonicalizationVersion)
	}
	if MaxOpaqueIDBytes != 255 || MaxActivationEntries != 100_000 || MaxCanonicalSetBytes != 64<<20 {
		t.Fatalf("limits = %d/%d/%d", MaxOpaqueIDBytes, MaxActivationEntries, MaxCanonicalSetBytes)
	}
}

func TestValidateSetAcceptsCompleteCoherentBindings(t *testing.T) {
	set := contractValidSet(t)
	if set.CatalogSchema.CatalogSnapshot.CatalogRevision == set.RoutingSecurity.SecurityEpoch {
		t.Fatal("fixture must prove CatalogRevision and SecurityEpoch are independent axes")
	}
	if err := ValidateSet(set); err != nil {
		t.Fatalf("ValidateSet: %v", err)
	}
}

func TestValidateSetRejectsInvalidOrForeignBinding(t *testing.T) {
	tests := map[string]func(*Set){
		"canonical version": func(set *Set) { set.CanonicalizationVersion = "v1" },
		"set org":           func(set *Set) { set.OrgID = "org-other" },
		"catalog org":       func(set *Set) { set.CatalogSchema.CatalogSnapshot.OrgID = "org-other" },
		"security org":      func(set *Set) { set.RoutingSecurity.OrgID = "org-other" },
		"catalog revision":  func(set *Set) { set.CatalogSchema.CatalogSnapshot.CatalogRevision = 0 },
		"catalog digest":    func(set *Set) { set.CatalogSchema.CatalogSnapshot.CatalogSnapshotDigest = "invalid" },
		"schema generation": func(set *Set) { set.CatalogSchema.CatalogSnapshot.SchemaGeneration = 0 },
		"engine generation": func(set *Set) { set.CatalogSchema.CatalogSnapshot.EngineGeneration = 0 },
		"validation view":   func(set *Set) { set.CatalogSchema.LayoutValidationViewDigest = "invalid" },
		"security epoch":    func(set *Set) { set.RoutingSecurity.SecurityEpoch = 0 },
		"security view":     func(set *Set) { set.RoutingSecurity.LayoutRoutingSecurityViewDigest = "invalid" },
		"required digest":   func(set *Set) { set.RequiredBundleKeySetDigest = "invalid" },
		"provenance digest": func(set *Set) { set.PreparationProvenanceDigest = "invalid" },
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			set := contractValidSet(t)
			mutate(&set)
			contractAssertInvalidSet(t, set)
		})
	}

	set := contractValidSet(t)
	set.OrgID = "org-other"
	set.CatalogSchema.CatalogSnapshot.OrgID = "org-other"
	contractAssertInvalidSet(t, set)
	set.RoutingSecurity.OrgID = "org-other"
	if err := ValidateSet(set); err != nil {
		t.Fatalf("same non-default OrgID rejected: %v", err)
	}
}

func TestValidateSetDistinguishesAbsentAndExplicitEmptyEntries(t *testing.T) {
	absent := contractValidSet(t)
	absent.Entries = nil
	contractAssertInvalidSet(t, absent)

	empty := contractValidSet(t)
	empty.Entries = []Entry{}
	contractRecomputeRequiredDigest(t, &empty)
	if err := ValidateSet(empty); err != nil {
		t.Fatalf("explicit empty set rejected: %v", err)
	}
	if _, err := CanonicalSetBytes(empty); err != nil {
		t.Fatalf("canonical empty set rejected: %v", err)
	}

	empty.RequiredBundleKeySetDigest = contractValidSet(t).RequiredBundleKeySetDigest
	contractAssertInvalidSet(t, empty)
}

func TestValidateSetRejectsCountBeforeInspectingEntries(t *testing.T) {
	set := contractValidSet(t)
	set.Entries = make([]Entry, MaxActivationEntries+1)
	err := ValidateSet(set)
	if err == nil || !strings.Contains(err.Error(), "exceeds") {
		t.Fatalf("count overflow error = %v", err)
	}
}

func TestValidateSetAcceptsExactMaximumEntryCountUnderByteLimit(t *testing.T) {
	set := contractValidSet(t)
	set.Entries = contractUniqueEntries(MaxActivationEntries)
	contractRecomputeRequiredDigest(t, &set)
	size, err := canonicalSetSize(set)
	if err != nil {
		t.Fatal(err)
	}
	if size >= MaxCanonicalSetBytes {
		t.Fatalf("maximum-count fixture size = %d, want below %d", size, MaxCanonicalSetBytes)
	}
	if err := ValidateSet(set); err != nil {
		t.Fatalf("exact %d-entry set rejected: %v", MaxActivationEntries, err)
	}
}

func TestValidateSetRejectsCanonicalSizeBeforeInspectingEntries(t *testing.T) {
	set := contractValidSet(t)
	maxNamespace := strings.Repeat("a", 63) + "." + strings.Repeat("b", 62) + ".c"
	if len(maxNamespace) != layoutbundlekeyv1.MaxSlotOwnerNamespaceBytes {
		t.Fatalf("maximum namespace fixture has %d bytes", len(maxNamespace))
	}
	oversizedEntry := Entry{
		BundleKey: layoutbundlekeyv1.BundleKey{
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindTenant,
			SlotOwnerNamespace: maxNamespace,
			LocalComponentKey:  contractLocalKey(1),
		},
		LayoutBundleRevisionID:   LayoutBundleRevisionID(strings.Repeat("r", MaxOpaqueIDBytes)),
		LayoutValidationID:       LayoutValidationID(strings.Repeat("v", MaxOpaqueIDBytes)),
		ResolvedLayoutViewDigest: contractDigest('d'),
	}
	set.Entries = make([]Entry, MaxActivationEntries)
	for index := range set.Entries {
		set.Entries[index] = oversizedEntry
	}
	err := ValidateSet(set)
	if err == nil || !strings.Contains(err.Error(), "canonical bytes exceed") {
		t.Fatalf("size overflow error = %v", err)
	}
}

func TestValidateSetEnforcesBundleKeyGrammar(t *testing.T) {
	tests := map[string]layoutbundlekeyv1.BundleKey{
		"unknown owner": {
			SlotOwnerKind:      "APP",
			SlotOwnerNamespace: "io.example.app",
			LocalComponentKey:  contractLocalKey(1),
		},
		"reserved app namespace": {
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindApp,
			SlotOwnerNamespace: layoutbundlekeyv1.SystemSlotOwnerNamespace,
			LocalComponentKey:  contractLocalKey(1),
		},
		"bad system namespace": {
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindSystem,
			SlotOwnerNamespace: "io.nexuscrm.other",
			LocalComponentKey:  contractLocalKey(1),
		},
		"bad local key": {
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindTenant,
			SlotOwnerNamespace: "io.example",
			LocalComponentKey:  "sha256:" + layoutbundlekeyv1.LocalComponentKey(strings.Repeat("1", 64)),
		},
	}
	for name, key := range tests {
		t.Run(name, func(t *testing.T) {
			set := contractValidSet(t)
			set.Entries[0].BundleKey = key
			contractAssertInvalidSet(t, set)
		})
	}
}

func TestValidateSetRejectsDuplicateTypedIdentities(t *testing.T) {
	tests := map[string]func([]Entry){
		"bundle key": func(entries []Entry) {
			entries[1].BundleKey = entries[0].BundleKey
		},
		"revision ID": func(entries []Entry) {
			entries[1].LayoutBundleRevisionID = entries[0].LayoutBundleRevisionID
		},
		"validation ID": func(entries []Entry) {
			entries[1].LayoutValidationID = entries[0].LayoutValidationID
		},
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			set := contractValidSet(t)
			mutate(set.Entries)
			contractAssertInvalidSet(t, set)
		})
	}

	set := contractValidSet(t)
	set.Entries[0].LayoutValidationID = LayoutValidationID(set.Entries[0].LayoutBundleRevisionID)
	if err := ValidateSet(set); err != nil {
		t.Fatalf("equal opaque bytes in distinct type domains rejected: %v", err)
	}
}

func TestOpaqueIDValidationPinsExactUTF8BytesAndBounds(t *testing.T) {
	valid := []string{
		"x",
		strings.Repeat("x", MaxOpaqueIDBytes),
		"revision with internal spaces",
		"Révision/版本",
		"\u00a0non-ASCII-space-boundary\u00a0",
	}
	for _, value := range valid {
		if err := ValidateLayoutBundleRevisionID(LayoutBundleRevisionID(value)); err != nil {
			t.Fatalf("valid revision ID %q rejected: %v", value, err)
		}
		if err := ValidateLayoutValidationID(LayoutValidationID(value)); err != nil {
			t.Fatalf("valid validation ID %q rejected: %v", value, err)
		}
	}

	invalid := []string{
		"",
		strings.Repeat("x", MaxOpaqueIDBytes+1),
		" leading",
		"trailing ",
		"embedded\x00nul",
		"embedded\x1fcontrol",
		"embedded\x7fdel",
		string([]byte{0xff}),
	}
	for _, value := range invalid {
		if err := ValidateLayoutBundleRevisionID(LayoutBundleRevisionID(value)); err == nil {
			t.Fatalf("invalid revision ID %q accepted", value)
		}
		if err := ValidateLayoutValidationID(LayoutValidationID(value)); err == nil {
			t.Fatalf("invalid validation ID %q accepted", value)
		}
	}
}

func TestValidateSetRejectsInvalidEntryValues(t *testing.T) {
	tests := map[string]func(*Entry){
		"empty revision": func(entry *Entry) { entry.LayoutBundleRevisionID = "" },
		"long revision": func(entry *Entry) {
			entry.LayoutBundleRevisionID = LayoutBundleRevisionID(strings.Repeat("x", MaxOpaqueIDBytes+1))
		},
		"invalid revision": func(entry *Entry) { entry.LayoutBundleRevisionID = LayoutBundleRevisionID(string([]byte{0xff})) },
		"empty validation": func(entry *Entry) { entry.LayoutValidationID = "" },
		"long validation": func(entry *Entry) {
			entry.LayoutValidationID = LayoutValidationID(strings.Repeat("x", MaxOpaqueIDBytes+1))
		},
		"invalid validation": func(entry *Entry) {
			entry.LayoutValidationID = LayoutValidationID(string([]byte{0xff}))
		},
		"resolved digest": func(entry *Entry) { entry.ResolvedLayoutViewDigest = "invalid" },
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			set := contractValidSet(t)
			mutate(&set.Entries[0])
			contractAssertInvalidSet(t, set)
		})
	}
}

func TestValidateSetRecomputesCompleteRequiredBundleKeySet(t *testing.T) {
	set := contractValidSet(t)
	if err := ValidateSet(set); err != nil {
		t.Fatal(err)
	}

	missingDigest := set
	missingDigest.Entries = append([]Entry(nil), set.Entries[1:]...)
	contractAssertInvalidSet(t, missingDigest)

	extraEntry := set
	extraEntry.Entries = append(append([]Entry(nil), set.Entries...), contractEntry(layoutbundlekeyv1.SlotOwnerKindTenant, 90))
	contractAssertInvalidSet(t, extraEntry)

	substituted := contractCloneSet(set)
	substituted.Entries[0].BundleKey.LocalComponentKey = contractLocalKey(91)
	contractAssertInvalidSet(t, substituted)

	reordered := contractCloneSet(set)
	for left, right := 0, len(reordered.Entries)-1; left < right; left, right = left+1, right-1 {
		reordered.Entries[left], reordered.Entries[right] = reordered.Entries[right], reordered.Entries[left]
	}
	if err := ValidateSet(reordered); err != nil {
		t.Fatalf("entry permutation changed required-set identity: %v", err)
	}
}

func TestCanonicalizeSetSortsByPortableBundleKeyAndDoesNotMutate(t *testing.T) {
	set := contractValidSet(t)
	before := append([]Entry(nil), set.Entries...)
	canonical, err := CanonicalizeSet(set)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(set.Entries, before) {
		t.Fatal("CanonicalizeSet mutated caller entries")
	}

	wantKinds := []layoutbundlekeyv1.SlotOwnerKind{
		layoutbundlekeyv1.SlotOwnerKindApp,
		layoutbundlekeyv1.SlotOwnerKindApp,
		layoutbundlekeyv1.SlotOwnerKindTenant,
		layoutbundlekeyv1.SlotOwnerKindSystem,
	}
	for index, want := range wantKinds {
		if got := canonical.Entries[index].BundleKey.SlotOwnerKind; got != want {
			t.Fatalf("canonical owner kind[%d] = %q, want %q", index, got, want)
		}
	}
	if string(canonical.Entries[0].BundleKey.LocalComponentKey) >= string(canonical.Entries[1].BundleKey.LocalComponentKey) {
		t.Fatal("same-owner keys are not ordered by raw local-component-key bytes")
	}

	canonical.Entries[0] = contractEntry(layoutbundlekeyv1.SlotOwnerKindSystem, 99)
	if !reflect.DeepEqual(set.Entries, before) {
		t.Fatal("canonical output shares caller slice storage")
	}

	permuted := contractCloneSet(set)
	for left, right := 0, len(permuted.Entries)-1; left < right; left, right = left+1, right-1 {
		permuted.Entries[left], permuted.Entries[right] = permuted.Entries[right], permuted.Entries[left]
	}
	forward, err := CanonicalSetBytes(set)
	if err != nil {
		t.Fatal(err)
	}
	reverse, err := CanonicalSetBytes(permuted)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(forward, reverse) {
		t.Fatal("entry permutation changed canonical bytes")
	}
	forwardDigest, err := SetDigest(set)
	if err != nil {
		t.Fatal(err)
	}
	reverseDigest, err := SetDigest(permuted)
	if err != nil {
		t.Fatal(err)
	}
	if forwardDigest != reverseDigest {
		t.Fatal("entry permutation changed digest")
	}
}

func TestSetDigestCommitsEveryBindingAndEntryAxis(t *testing.T) {
	base := contractValidSet(t)
	baseDigest, err := SetDigest(base)
	if err != nil {
		t.Fatal(err)
	}

	mutations := map[string]func(*Set){
		"org": func(set *Set) {
			set.OrgID = "org-other"
			set.CatalogSchema.CatalogSnapshot.OrgID = set.OrgID
			set.RoutingSecurity.OrgID = set.OrgID
		},
		"catalog revision": func(set *Set) { set.CatalogSchema.CatalogSnapshot.CatalogRevision++ },
		"catalog snapshot digest": func(set *Set) {
			set.CatalogSchema.CatalogSnapshot.CatalogSnapshotDigest = contractDigest('8')
		},
		"schema generation": func(set *Set) { set.CatalogSchema.CatalogSnapshot.SchemaGeneration++ },
		"engine generation": func(set *Set) { set.CatalogSchema.CatalogSnapshot.EngineGeneration++ },
		"layout validation view": func(set *Set) {
			set.CatalogSchema.LayoutValidationViewDigest = contractDigest('9')
		},
		"security epoch": func(set *Set) { set.RoutingSecurity.SecurityEpoch++ },
		"routing security view": func(set *Set) {
			set.RoutingSecurity.LayoutRoutingSecurityViewDigest = contractDigest('a')
		},
		"preparation provenance": func(set *Set) { set.PreparationProvenanceDigest = contractDigest('b') },
		"required key set and bundle key": func(set *Set) {
			set.Entries[0].BundleKey.LocalComponentKey = contractLocalKey(92)
			contractRecomputeRequiredDigest(t, set)
		},
		"revision ID":   func(set *Set) { set.Entries[0].LayoutBundleRevisionID += "-changed" },
		"validation ID": func(set *Set) { set.Entries[0].LayoutValidationID += "-changed" },
		"resolved view": func(set *Set) { set.Entries[0].ResolvedLayoutViewDigest = contractDigest('c') },
	}
	for name, mutate := range mutations {
		t.Run(name, func(t *testing.T) {
			changed := contractCloneSet(base)
			mutate(&changed)
			if err := ValidateSet(changed); err != nil {
				t.Fatalf("mutation produced invalid set: %v", err)
			}
			digest, err := SetDigest(changed)
			if err != nil {
				t.Fatal(err)
			}
			if digest == baseDigest {
				t.Fatal("field change did not change digest")
			}
		})
	}
}

func TestCanonicalUnsignedLEB128Encoding(t *testing.T) {
	tests := []struct {
		value uint64
		hex   string
	}{
		{0, "00"},
		{1, "01"},
		{127, "7f"},
		{128, "8001"},
		{255, "ff01"},
		{300, "ac02"},
		{16_384, "808001"},
		{math.MaxUint64, "ffffffffffffffffff01"},
	}
	for _, test := range tests {
		var out bytes.Buffer
		writeCanonicalUint(&out, test.value)
		if got := hex.EncodeToString(out.Bytes()); got != test.hex {
			t.Fatalf("U(%d) = %s, want %s", test.value, got, test.hex)
		}
	}
}

func TestCanonicalSetPinsUnsignedLEB128EntryCountBoundary(t *testing.T) {
	for _, test := range []struct {
		count int
		want  []byte
	}{
		{127, []byte{0x7f}},
		{128, []byte{0x80, 0x01}},
	} {
		t.Run(fmt.Sprintf("count_%d", test.count), func(t *testing.T) {
			set := contractValidSet(t)
			set.Entries = contractUniqueEntries(test.count)
			contractRecomputeRequiredDigest(t, &set)
			canonical, err := CanonicalSetBytes(set)
			if err != nil {
				t.Fatal(err)
			}
			headerOnly := set
			headerOnly.Entries = []Entry{}
			headerSize, err := canonicalSetSize(headerOnly)
			if err != nil {
				t.Fatal(err)
			}
			countOffset := headerSize - canonicalUintSize(0)
			got := canonical[countOffset : countOffset+uint64(len(test.want))]
			if !bytes.Equal(got, test.want) {
				t.Fatalf("entry count bytes = %x, want %x", got, test.want)
			}
		})
	}
}

func TestCanonicalSetSizeHelperMatchesOutputAndEnforcesLimit(t *testing.T) {
	set := contractValidSet(t)
	canonical, err := CanonicalSetBytes(set)
	if err != nil {
		t.Fatal(err)
	}
	size, err := canonicalSetSize(set)
	if err != nil {
		t.Fatal(err)
	}
	if size != uint64(len(canonical)) {
		t.Fatalf("canonicalSetSize = %d, output length = %d", size, len(canonical))
	}
	if err := validateCanonicalSetSize(set, size); err != nil {
		t.Fatalf("exact byte limit rejected: %v", err)
	}
	if err := validateCanonicalSetSize(set, size-1); err == nil {
		t.Fatal("set exceeding a lower test limit was accepted")
	}
}

func TestCanonicalSizeAccumulatorFailsClosedOnUint64Overflow(t *testing.T) {
	measure := canonicalSizeMeasure{size: ^uint64(0) - 1, limit: ^uint64(0)}
	measure.add(2)
	if !measure.exceeded {
		t.Fatal("uint64 size overflow was accepted")
	}
}

func TestSetPublicOperationsFailClosedOnInvalidInput(t *testing.T) {
	set := contractValidSet(t)
	set.Entries = nil
	if _, err := CanonicalizeSet(set); err == nil {
		t.Fatal("CanonicalizeSet accepted invalid input")
	}
	if _, err := CanonicalSetBytes(set); err == nil {
		t.Fatal("CanonicalSetBytes accepted invalid input")
	}
	if _, err := SetDigest(set); err == nil {
		t.Fatal("SetDigest accepted invalid input")
	}
}

func contractValidSet(t *testing.T) Set {
	t.Helper()
	set := Set{
		CanonicalizationVersion: CanonicalizationVersion,
		OrgID:                   "org-alpha",
		CatalogSchema: catalogpublicationv1.CatalogSchemaBinding{
			CatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
				OrgID:                 "org-alpha",
				CatalogRevision:       17,
				CatalogSnapshotDigest: contractDigest('1'),
				SchemaGeneration:      23,
				EngineGeneration:      31,
			},
			LayoutValidationViewDigest: contractDigest('2'),
		},
		RoutingSecurity: layoutroutingsecurityv1.Binding{
			OrgID:                           "org-alpha",
			SecurityEpoch:                   43,
			LayoutRoutingSecurityViewDigest: contractDigest('3'),
		},
		PreparationProvenanceDigest: contractDigest('4'),
		Entries: []Entry{
			contractEntry(layoutbundlekeyv1.SlotOwnerKindSystem, 4),
			contractEntry(layoutbundlekeyv1.SlotOwnerKindApp, 2),
			contractEntry(layoutbundlekeyv1.SlotOwnerKindTenant, 3),
			contractEntry(layoutbundlekeyv1.SlotOwnerKindApp, 1),
		},
	}
	contractRecomputeRequiredDigest(t, &set)
	return set
}

func contractEntry(kind layoutbundlekeyv1.SlotOwnerKind, number int) Entry {
	namespace := "io.example.app"
	switch kind {
	case layoutbundlekeyv1.SlotOwnerKindTenant:
		namespace = "io.example"
	case layoutbundlekeyv1.SlotOwnerKindSystem:
		namespace = layoutbundlekeyv1.SystemSlotOwnerNamespace
	}
	return Entry{
		BundleKey: layoutbundlekeyv1.BundleKey{
			SlotOwnerKind:      kind,
			SlotOwnerNamespace: namespace,
			LocalComponentKey:  contractLocalKey(number),
		},
		LayoutBundleRevisionID:   LayoutBundleRevisionID(fmt.Sprintf("revision-%03d", number)),
		LayoutValidationID:       LayoutValidationID(fmt.Sprintf("validation-%03d", number)),
		ResolvedLayoutViewDigest: contractDigest(byte("def56789abcdef01"[number%16])),
	}
}

func contractUniqueEntries(count int) []Entry {
	entries := make([]Entry, count)
	for index := range entries {
		entries[index] = contractEntry(layoutbundlekeyv1.SlotOwnerKindTenant, index+1)
	}
	return entries
}

func contractLocalKey(number int) layoutbundlekeyv1.LocalComponentKey {
	return layoutbundlekeyv1.LocalComponentKey(fmt.Sprintf("%s%064x", layoutbundlekeyv1.LocalComponentKeyPrefix, number))
}

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

func contractRecomputeRequiredDigest(t *testing.T, set *Set) {
	t.Helper()
	keys := make([]layoutbundlekeyv1.BundleKey, len(set.Entries))
	for index, entry := range set.Entries {
		keys[index] = entry.BundleKey
	}
	digest, err := layoutbundlekeyv1.RequiredBundleKeySetDigest(layoutbundlekeyv1.RequiredBundleKeySet{
		Version:    layoutbundlekeyv1.RequiredBundleKeySetVersion,
		BundleKeys: keys,
	})
	if err != nil {
		t.Fatalf("required bundle-key digest fixture: %v", err)
	}
	set.RequiredBundleKeySetDigest = digest
}

func contractCloneSet(set Set) Set {
	clone := set
	clone.Entries = append([]Entry(nil), set.Entries...)
	return clone
}

func contractAssertInvalidSet(t *testing.T, set Set) {
	t.Helper()
	if err := ValidateSet(set); err == nil {
		t.Fatalf("ValidateSet(%#v) succeeded", set)
	}
}

func contractAssertExactFields(t *testing.T, typ reflect.Type, want []string) {
	t.Helper()
	if typ.NumField() != len(want) {
		t.Fatalf("%s has %d fields, want %d", typ.Name(), typ.NumField(), len(want))
	}
	for index, name := range want {
		if got := typ.Field(index).Name; got != name {
			t.Fatalf("%s field[%d] = %q, want %q", typ.Name(), index, got, name)
		}
	}
}
