package catalogassignmentidentityv1

import (
	"bytes"
	"fmt"
	"math"
	"reflect"
	"strings"
	"testing"

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

const (
	testInstallationID1 = "0190d800-0000-7000-8000-000000000001"
	testInstallationID2 = "0190d800-0000-7000-8000-000000000002"
)

func TestNormalizeObjectAPIName(t *testing.T) {
	if MaxObjectAPINameBytes != 64 {
		t.Fatalf("MaxObjectAPINameBytes = %d, want storage-independent v1 semantic limit 64", MaxObjectAPINameBytes)
	}
	tests := []struct {
		name  string
		input string
		want  string
	}{
		{name: "standard", input: "Account", want: "account"},
		{name: "custom", input: "Sales_Engagement__c", want: "sales_engagement__c"},
		{name: "single underscore", input: "_", want: "_"},
		{name: "custom suffix only", input: "__c", want: "__c"},
		{name: "leading underscore", input: "_Private9", want: "_private9"},
		{name: "maximum length", input: "A" + strings.Repeat("b", MaxObjectAPINameBytes-1), want: "a" + strings.Repeat("b", MaxObjectAPINameBytes-1)},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got, err := NormalizeObjectAPIName(test.input)
			if err != nil {
				t.Fatalf("NormalizeObjectAPIName(%q): %v", test.input, err)
			}
			if got.NormalizedAPIName != test.want {
				t.Fatalf("NormalizeObjectAPIName(%q) = %q, want %q", test.input, got.NormalizedAPIName, test.want)
			}
			if err := ValidateObjectKey(got); err != nil {
				t.Fatalf("normalized key is not valid: %v", err)
			}
		})
	}

	left, err := NormalizeObjectAPIName("Account")
	if err != nil {
		t.Fatal(err)
	}
	right, err := NormalizeObjectAPIName("account")
	if err != nil {
		t.Fatal(err)
	}
	if left != right {
		t.Fatalf("case variants did not collide: %#v != %#v", left, right)
	}

	for name, input := range map[string]string{
		"empty":            "",
		"overlong":         "a" + strings.Repeat("b", MaxObjectAPINameBytes),
		"leading digit":    "9Account",
		"hyphen":           "Sales-Account",
		"dot":              "Sales.Account",
		"space":            "Sales Account",
		"unicode":          "Accöunt",
		"invalid UTF-8":    string([]byte{0xff}),
		"leading newline":  "\nAccount",
		"trailing newline": "Account\n",
	} {
		t.Run("reject "+name, func(t *testing.T) {
			if _, err := NormalizeObjectAPIName(input); err == nil {
				t.Fatalf("NormalizeObjectAPIName(%q) unexpectedly succeeded", input)
			}
		})
	}

	for name, key := range map[string]ObjectKey{
		"empty":         {},
		"uppercase":     {NormalizedAPIName: "Account"},
		"leading digit": {NormalizedAPIName: "9account"},
		"unicode":       {NormalizedAPIName: "accöunt"},
		"overlong":      {NormalizedAPIName: "a" + strings.Repeat("b", MaxObjectAPINameBytes)},
	} {
		t.Run("reject key "+name, func(t *testing.T) {
			if err := ValidateObjectKey(key); err == nil {
				t.Fatalf("ValidateObjectKey(%#v) unexpectedly succeeded", key)
			}
		})
	}
}

func TestKeyValidationAndReservedMasterRules(t *testing.T) {
	longLocalKey := strings.Repeat("a", MaxKeySegmentBytes) + "." + strings.Repeat("b", MaxKeySegmentBytes)
	validRecordKeys := []RecordTypeKey{
		{NamespaceID: SystemNamespaceID, LocalKey: MasterRecordTypeLocalKey},
		{NamespaceID: "io.pingcap.sales", LocalKey: "record-type.enterprise"},
		{NamespaceID: "io.pingcap.sales", LocalKey: longLocalKey},
	}
	for _, key := range validRecordKeys {
		if err := ValidateRecordTypeKey(key); err != nil {
			t.Errorf("ValidateRecordTypeKey(%#v): %v", key, err)
		}
	}

	if err := ValidateRecordTypeKey(RecordTypeKey{
		NamespaceID: "io.pingcap.sales",
		LocalKey:    MasterRecordTypeLocalKey,
	}); err == nil {
		t.Fatal("Master record-type key outside the system namespace unexpectedly succeeded")
	}
	if err := ValidateNavigationAppKey(NavigationAppKey{
		NamespaceID: SystemNamespaceID,
		LocalKey:    MasterRecordTypeLocalKey,
	}); err == nil {
		t.Fatal("reserved Master record-type pair unexpectedly succeeded as a navigation key")
	}
	if err := ValidateNavigationAppKey(NavigationAppKey{
		NamespaceID: SystemNamespaceID,
		LocalKey:    "navigation.admin",
	}); err != nil {
		t.Fatalf("non-reserved system navigation key was rejected: %v", err)
	}

	for name, key := range map[string]RecordTypeKey{
		"empty namespace":     {LocalKey: "record-type.enterprise"},
		"short namespace":     {NamespaceID: "sales", LocalKey: "record-type.enterprise"},
		"uppercase namespace": {NamespaceID: "io.Pingcap.sales", LocalKey: "record-type.enterprise"},
		"empty local key":     {NamespaceID: "io.pingcap.sales"},
		"uppercase local key": {NamespaceID: "io.pingcap.sales", LocalKey: "Record-type.enterprise"},
		"empty segment":       {NamespaceID: "io.pingcap.sales", LocalKey: "record-type..enterprise"},
		"trailing hyphen":     {NamespaceID: "io.pingcap.sales", LocalKey: "record-type.enterprise-"},
		"overlong segment":    {NamespaceID: "io.pingcap.sales", LocalKey: strings.Repeat("a", MaxKeySegmentBytes+1)},
		"overlong key":        {NamespaceID: "io.pingcap.sales", LocalKey: longLocalKey + ".c"},
	} {
		t.Run("reject record key "+name, func(t *testing.T) {
			if err := ValidateRecordTypeKey(key); err == nil {
				t.Fatalf("ValidateRecordTypeKey(%#v) unexpectedly succeeded", key)
			}
		})
	}

	if err := ValidateProductAppKey(ProductAppKey{AppID: "io.pingcap.sales"}); err != nil {
		t.Fatalf("valid product App key was rejected: %v", err)
	}
	for _, appID := range []string{"", "io.pingcap", "io.Pingcap.sales", "io.pingcap.sales-"} {
		if err := ValidateProductAppKey(ProductAppKey{AppID: appID}); err == nil {
			t.Errorf("invalid product App key %q unexpectedly succeeded", appID)
		}
	}
}

func TestValidateViewAcceptsCompleteAndExplicitlyEmptyViews(t *testing.T) {
	view := validAssignmentView()
	if err := ValidateView(view); err != nil {
		t.Fatalf("ValidateView(valid): %v", err)
	}

	empty := validAssignmentView()
	empty.Objects = []ObjectIdentity{}
	empty.RecordTypes = []RecordTypeIdentity{}
	empty.ProductApps = []ProductAppIdentity{}
	empty.NavigationApps = []NavigationAppIdentity{}
	if err := ValidateView(empty); err != nil {
		t.Fatalf("ValidateView(explicit empty): %v", err)
	}

	tombstoned := validAssignmentView()
	tombstoned.Objects[0].State = IdentityStateTombstone
	tombstoned.RecordTypes[0].State = IdentityStateTombstone
	tombstoned.ProductApps[0].State = IdentityStateTombstone
	tombstoned.NavigationApps[0].State = IdentityStateTombstone
	if err := ValidateView(tombstoned); err != nil {
		t.Fatalf("ValidateView(tombstoned identities): %v", err)
	}

	neverSelected := validAssignmentView()
	neverSelected.ProductApps[0].State = IdentityStateTombstone
	neverSelected.ProductApps[0].ResolvedTarget = absentProductAppTarget()
	if err := ValidateView(neverSelected); err != nil {
		t.Fatalf("ValidateView(never-selected Product App tombstone): %v", err)
	}

	multipleNeverSelected := neverSelected
	multipleNeverSelected.ProductApps = append([]ProductAppIdentity{}, neverSelected.ProductApps...)
	multipleNeverSelected.ProductApps = append(multipleNeverSelected.ProductApps, ProductAppIdentity{
		Key:            ProductAppKey{AppID: "io.pingcap.service"},
		State:          IdentityStateTombstone,
		ResolvedTarget: absentProductAppTarget(),
	})
	if err := ValidateView(multipleNeverSelected); err != nil {
		t.Fatalf("multiple ABSENT Product-App tombstones were rejected: %v", err)
	}

	sharedTargets := validAssignmentView()
	sharedTargets.Objects[0].ResolvedObjectID = "shared-target"
	sharedTargets.RecordTypes[0].ResolvedRecordTypeID = "shared-target"
	sharedTargets.NavigationApps[0].ResolvedAppConfigID = "shared-target"
	if err := ValidateView(sharedTargets); err != nil {
		t.Fatalf("target bytes shared across dimensions were rejected: %v", err)
	}
}

func TestValidateViewRejectsInvalidEnvelopeAndNilArrays(t *testing.T) {
	for name, mutate := range map[string]func(*View){
		"empty org":               func(view *View) { view.SourceCatalogSnapshot.OrgID = "" },
		"zero Catalog revision":   func(view *View) { view.SourceCatalogSnapshot.CatalogRevision = 0 },
		"invalid snapshot digest": func(view *View) { view.SourceCatalogSnapshot.CatalogSnapshotDigest = "sha256:bad" },
		"zero schema generation":  func(view *View) { view.SourceCatalogSnapshot.SchemaGeneration = 0 },
		"zero engine generation":  func(view *View) { view.SourceCatalogSnapshot.EngineGeneration = 0 },
		"wrong canonical version": func(view *View) { view.CanonicalizationVersion = "v2" },
		"wrong key grammar":       func(view *View) { view.KeyGrammarVersion = "v2" },
		"wrong normalization":     func(view *View) { view.ObjectKeyNormalizationVersion = "v2" },
	} {
		t.Run(name, func(t *testing.T) {
			view := validAssignmentView()
			mutate(&view)
			if err := ValidateView(view); err == nil {
				t.Fatal("ValidateView unexpectedly succeeded")
			}
		})
	}

	for name, mutate := range map[string]func(*View){
		"objects":         func(view *View) { view.Objects = nil },
		"record types":    func(view *View) { view.RecordTypes = nil },
		"product Apps":    func(view *View) { view.ProductApps = nil },
		"navigation Apps": func(view *View) { view.NavigationApps = nil },
	} {
		t.Run("nil "+name, func(t *testing.T) {
			view := validAssignmentView()
			mutate(&view)
			expectErrorContains(t, ValidateView(view), "identity arrays must be present")
		})
	}
}

func TestValidateViewChecksDimensionCapsBeforeEntriesInStableOrder(t *testing.T) {
	oversizedObjects := func(view *View) {
		view.Objects = make([]ObjectIdentity, MaxEntriesPerDimension+1)
	}
	oversizedRecordTypes := func(view *View) {
		view.RecordTypes = make([]RecordTypeIdentity, MaxEntriesPerDimension+1)
	}
	oversizedProductApps := func(view *View) {
		view.ProductApps = make([]ProductAppIdentity, MaxEntriesPerDimension+1)
	}
	oversizedNavigationApps := func(view *View) {
		view.NavigationApps = make([]NavigationAppIdentity, MaxEntriesPerDimension+1)
	}
	for _, test := range []struct {
		name   string
		mutate func(*View)
		want   string
	}{
		{name: "objects", mutate: oversizedObjects, want: "View.Objects exceeds"},
		{name: "record types", mutate: oversizedRecordTypes, want: "View.RecordTypes exceeds"},
		{name: "product Apps", mutate: oversizedProductApps, want: "View.ProductApps exceeds"},
		{name: "navigation Apps", mutate: oversizedNavigationApps, want: "View.NavigationApps exceeds"},
	} {
		t.Run(test.name, func(t *testing.T) {
			view := validAssignmentView()
			test.mutate(&view)
			expectErrorContains(t, ValidateView(view), test.want)
		})
	}

	view := validAssignmentView()
	oversizedObjects(&view)
	oversizedRecordTypes(&view)
	oversizedProductApps(&view)
	oversizedNavigationApps(&view)
	for attempt := 0; attempt < 10; attempt++ {
		expectErrorContains(t, ValidateView(view), "View.Objects exceeds")
	}
}

func TestValidateViewAcceptsTheExactDimensionLimit(t *testing.T) {
	view := validAssignmentView()
	view.Objects = make([]ObjectIdentity, MaxEntriesPerDimension)
	view.RecordTypes = []RecordTypeIdentity{}
	view.ProductApps = []ProductAppIdentity{}
	view.NavigationApps = []NavigationAppIdentity{}
	for index := range view.Objects {
		view.Objects[index] = ObjectIdentity{
			Key:              ObjectKey{NormalizedAPIName: fmt.Sprintf("o%05d", index)},
			ResolvedObjectID: fmt.Sprintf("id-%05d", index),
			State:            IdentityStateActive,
		}
	}
	if err := ValidateView(view); err != nil {
		t.Fatalf("exact per-dimension limit was rejected: %v", err)
	}
}

func TestValidateViewRejectsDuplicateKeysAndTargetIDsWithinDimensions(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*View)
		want   string
	}{
		{
			name: "object key", want: "duplicate ObjectKey",
			mutate: func(view *View) {
				view.Objects = append(view.Objects, ObjectIdentity{Key: ObjectKey{NormalizedAPIName: "account"}, ResolvedObjectID: "obj-other", State: IdentityStateActive})
			},
		},
		{
			name: "object target", want: "duplicate ResolvedObjectID",
			mutate: func(view *View) {
				view.Objects = append(view.Objects, ObjectIdentity{Key: ObjectKey{NormalizedAPIName: "contact"}, ResolvedObjectID: "obj-account", State: IdentityStateActive})
			},
		},
		{
			name: "record-type complete key", want: "duplicate complete key",
			mutate: func(view *View) {
				view.RecordTypes = append(view.RecordTypes, RecordTypeIdentity{ObjectKey: ObjectKey{NormalizedAPIName: "account"}, Key: RecordTypeKey{NamespaceID: SystemNamespaceID, LocalKey: MasterRecordTypeLocalKey}, ResolvedRecordTypeID: "rt-other", State: IdentityStateActive})
			},
		},
		{
			name: "record-type target", want: "duplicate ResolvedRecordTypeID",
			mutate: func(view *View) {
				view.RecordTypes = append(view.RecordTypes, RecordTypeIdentity{ObjectKey: ObjectKey{NormalizedAPIName: "account"}, Key: RecordTypeKey{NamespaceID: "io.pingcap.sales", LocalKey: "record-type.enterprise"}, ResolvedRecordTypeID: "rt-master", State: IdentityStateActive})
			},
		},
		{
			name: "product App key", want: "duplicate ProductAppKey",
			mutate: func(view *View) {
				view.ProductApps = append(view.ProductApps, ProductAppIdentity{
					Key: ProductAppKey{AppID: "io.pingcap.sales"}, State: IdentityStateActive,
					ResolvedTarget: presentProductAppTarget(testInstallationID2),
				})
			},
		},
		{
			name: "product App target", want: "duplicate present ResolvedAppInstallationID",
			mutate: func(view *View) {
				view.ProductApps = append(view.ProductApps, ProductAppIdentity{
					Key: ProductAppKey{AppID: "io.pingcap.service"}, State: IdentityStateActive,
					ResolvedTarget: presentProductAppTarget(testInstallationID1),
				})
			},
		},
		{
			name: "navigation App key", want: "duplicate NavigationAppKey",
			mutate: func(view *View) {
				view.NavigationApps = append(view.NavigationApps, NavigationAppIdentity{Key: NavigationAppKey{NamespaceID: "io.pingcap.sales", LocalKey: "navigation.sales"}, ResolvedAppConfigID: "nav-other", State: IdentityStateActive})
			},
		},
		{
			name: "navigation App target", want: "duplicate ResolvedAppConfigID",
			mutate: func(view *View) {
				view.NavigationApps = append(view.NavigationApps, NavigationAppIdentity{Key: NavigationAppKey{NamespaceID: "io.pingcap.sales", LocalKey: "navigation.service"}, ResolvedAppConfigID: "nav-sales", State: IdentityStateActive})
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			view := validAssignmentView()
			test.mutate(&view)
			expectErrorContains(t, ValidateView(view), test.want)
		})
	}
}

func TestValidateViewTargetIDAndStateRules(t *testing.T) {
	invalidLocalIDs := []string{
		"",
		" leading",
		"trailing ",
		"line\nbreak",
		"delete\x7fbyte",
		string([]byte{0xff}),
		strings.Repeat("x", MaxTargetLocalIDBytes+1),
	}
	for _, value := range invalidLocalIDs {
		t.Run("object target "+strings.ReplaceAll(value, "\n", " newline "), func(t *testing.T) {
			view := validAssignmentView()
			view.Objects[0].ResolvedObjectID = value
			expectErrorContains(t, ValidateView(view), "ResolvedObjectID")
		})
	}

	view := validAssignmentView()
	view.Objects[0].ResolvedObjectID = strings.Repeat("x", MaxTargetLocalIDBytes)
	view.RecordTypes[0].ResolvedRecordTypeID = "记录类型"
	view.NavigationApps[0].ResolvedAppConfigID = "navigation-设置"
	if err := ValidateView(view); err != nil {
		t.Fatalf("valid bounded UTF-8 target IDs were rejected: %v", err)
	}

	view = validAssignmentView()
	view.RecordTypes[0].ResolvedRecordTypeID = ""
	expectErrorContains(t, ValidateView(view), "ResolvedRecordTypeID")
	view = validAssignmentView()
	view.NavigationApps[0].ResolvedAppConfigID = ""
	expectErrorContains(t, ValidateView(view), "ResolvedAppConfigID")

	for name, installationID := range map[string]string{
		"empty":         "",
		"wrong version": "0190d800-0000-6000-8000-000000000001",
		"wrong variant": "0190d800-0000-7000-7000-000000000001",
		"uppercase":     "0190D800-0000-7000-8000-000000000001",
	} {
		t.Run("installation ID "+name, func(t *testing.T) {
			view := validAssignmentView()
			view.ProductApps[0].ResolvedTarget.ResolvedAppInstallationID = installationID
			expectErrorContains(t, ValidateView(view), "ResolvedAppInstallationID")
		})
	}

	for name, mutate := range map[string]func(*View){
		"object":         func(view *View) { view.Objects[0].State = "UNKNOWN" },
		"record type":    func(view *View) { view.RecordTypes[0].State = "UNKNOWN" },
		"product App":    func(view *View) { view.ProductApps[0].State = "UNKNOWN" },
		"navigation App": func(view *View) { view.NavigationApps[0].State = "UNKNOWN" },
	} {
		t.Run("state "+name, func(t *testing.T) {
			view := validAssignmentView()
			mutate(&view)
			expectErrorContains(t, ValidateView(view), "State")
		})
	}
}

func TestValidateProductAppResolvedTargetStateCombinations(t *testing.T) {
	for _, test := range []struct {
		name   string
		mutate func(*ProductAppIdentity)
		want   string
	}{
		{
			name: "ACTIVE with ABSENT",
			mutate: func(identity *ProductAppIdentity) {
				identity.ResolvedTarget = absentProductAppTarget()
			},
			want: "ACTIVE requires APP_INSTALLATION_ID",
		},
		{
			name: "ABSENT with payload",
			mutate: func(identity *ProductAppIdentity) {
				identity.State = IdentityStateTombstone
				identity.ResolvedTarget = ProductAppResolvedTarget{
					Kind:                      ProductAppResolvedTargetKindAbsent,
					ResolvedAppInstallationID: testInstallationID1,
				}
			},
			want: "ABSENT must carry no ResolvedAppInstallationID",
		},
		{
			name: "unknown kind",
			mutate: func(identity *ProductAppIdentity) {
				identity.ResolvedTarget.Kind = ProductAppResolvedTargetKind(0x02)
			},
			want: "Kind must be ABSENT or APP_INSTALLATION_ID",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			view := validAssignmentView()
			test.mutate(&view.ProductApps[0])
			expectErrorContains(t, ValidateView(view), test.want)
		})
	}

	tombstoneWithHistory := validAssignmentView()
	tombstoneWithHistory.ProductApps[0].State = IdentityStateTombstone
	if err := ValidateView(tombstoneWithHistory); err != nil {
		t.Fatalf("TOMBSTONE+APP_INSTALLATION_ID was rejected: %v", err)
	}

	neverSelectedTombstone := validAssignmentView()
	neverSelectedTombstone.ProductApps[0].State = IdentityStateTombstone
	neverSelectedTombstone.ProductApps[0].ResolvedTarget = absentProductAppTarget()
	if err := ValidateView(neverSelectedTombstone); err != nil {
		t.Fatalf("TOMBSTONE+ABSENT was rejected: %v", err)
	}
}

func TestValidateViewRecordTypeParentRules(t *testing.T) {
	missing := validAssignmentView()
	missing.RecordTypes[0].ObjectKey = ObjectKey{NormalizedAPIName: "contact"}
	expectErrorContains(t, ValidateView(missing), "missing ObjectKey")

	activeUnderTombstone := validAssignmentView()
	activeUnderTombstone.Objects[0].State = IdentityStateTombstone
	expectErrorContains(t, ValidateView(activeUnderTombstone), "active under a tombstoned ObjectKey")

	tombstoneUnderTombstone := validAssignmentView()
	tombstoneUnderTombstone.Objects[0].State = IdentityStateTombstone
	tombstoneUnderTombstone.RecordTypes[0].State = IdentityStateTombstone
	if err := ValidateView(tombstoneUnderTombstone); err != nil {
		t.Fatalf("tombstoned record type under tombstoned object was rejected: %v", err)
	}

	sameKeyUnderDifferentObjects := validAssignmentView()
	sameKeyUnderDifferentObjects.Objects = append(sameKeyUnderDifferentObjects.Objects, ObjectIdentity{
		Key: ObjectKey{NormalizedAPIName: "contact"}, ResolvedObjectID: "obj-contact", State: IdentityStateActive,
	})
	sameKeyUnderDifferentObjects.RecordTypes = append(sameKeyUnderDifferentObjects.RecordTypes, RecordTypeIdentity{
		ObjectKey:            ObjectKey{NormalizedAPIName: "contact"},
		Key:                  RecordTypeKey{NamespaceID: SystemNamespaceID, LocalKey: MasterRecordTypeLocalKey},
		ResolvedRecordTypeID: "rt-contact-master", State: IdentityStateActive,
	})
	if err := ValidateView(sameKeyUnderDifferentObjects); err != nil {
		t.Fatalf("the same record-type key under a different object was rejected: %v", err)
	}

	caseSensitiveTargetIDs := validAssignmentView()
	caseSensitiveTargetIDs.Objects = []ObjectIdentity{
		{Key: ObjectKey{NormalizedAPIName: "account"}, ResolvedObjectID: "ID", State: IdentityStateActive},
		{Key: ObjectKey{NormalizedAPIName: "contact"}, ResolvedObjectID: "id", State: IdentityStateActive},
	}
	caseSensitiveTargetIDs.RecordTypes = []RecordTypeIdentity{}
	if err := ValidateView(caseSensitiveTargetIDs); err != nil {
		t.Fatalf("case-distinct target IDs were rejected: %v", err)
	}
}

func TestCanonicalizeViewIsPermutationInvariantAndNonMutating(t *testing.T) {
	view := multiEntryAssignmentView()
	original := cloneAssignmentView(view)
	reversed := reverseAssignmentView(view)

	canonical, err := CanonicalizeView(view)
	if err != nil {
		t.Fatalf("CanonicalizeView: %v", err)
	}
	if !reflect.DeepEqual(view, original) {
		t.Fatal("CanonicalizeView mutated its input")
	}
	assertObjectOrder(t, canonical.Objects, []string{"account", "contact"})
	assertRecordTypeOrder(t, canonical.RecordTypes, []string{
		"account\x00io.nexuscrm.system\x00record-type.master",
		"contact\x00io.pingcap.sales\x00record-type.enterprise",
	})
	assertProductAppOrder(t, canonical.ProductApps, []string{"io.pingcap.alpha", "io.pingcap.sales"})
	assertNavigationAppOrder(t, canonical.NavigationApps, []string{
		"io.pingcap.alpha\x00navigation.home",
		"io.pingcap.sales\x00navigation.sales",
	})

	leftBytes, err := CanonicalViewBytes(view)
	if err != nil {
		t.Fatal(err)
	}
	rightBytes, err := CanonicalViewBytes(reversed)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(leftBytes, rightBytes) {
		t.Fatal("canonical bytes changed under a slice permutation")
	}
	if !reflect.DeepEqual(view, original) {
		t.Fatal("CanonicalViewBytes mutated its input")
	}
	leftDigest, err := ViewDigest(view)
	if err != nil {
		t.Fatal(err)
	}
	rightDigest, err := ViewDigest(reversed)
	if err != nil {
		t.Fatal(err)
	}
	if leftDigest != rightDigest {
		t.Fatalf("digest changed under a slice permutation: %s != %s", leftDigest, rightDigest)
	}

	canonical.Objects[0].ResolvedObjectID = "changed"
	canonical.RecordTypes[0].ResolvedRecordTypeID = "changed"
	canonical.ProductApps[0].ResolvedTarget.ResolvedAppInstallationID = testInstallationID2
	canonical.NavigationApps[0].ResolvedAppConfigID = "changed"
	if !reflect.DeepEqual(view, original) {
		t.Fatal("canonical output aliases an input slice")
	}
}

func TestViewDigestIsSensitiveToEveryIdentityAxis(t *testing.T) {
	base := validAssignmentView()
	want, err := ViewDigest(base)
	if err != nil {
		t.Fatal(err)
	}
	if len(want) != len("sha256:")+64 || !strings.HasPrefix(want, "sha256:") {
		t.Fatalf("ViewDigest returned non-canonical digest %q", want)
	}

	tests := []struct {
		name   string
		mutate func(*View)
	}{
		{name: "source org", mutate: func(view *View) { view.SourceCatalogSnapshot.OrgID = "org-2" }},
		{name: "Catalog revision", mutate: func(view *View) { view.SourceCatalogSnapshot.CatalogRevision++ }},
		{name: "snapshot digest", mutate: func(view *View) { view.SourceCatalogSnapshot.CatalogSnapshotDigest = testDigest("b") }},
		{name: "schema generation", mutate: func(view *View) { view.SourceCatalogSnapshot.SchemaGeneration++ }},
		{name: "engine generation", mutate: func(view *View) { view.SourceCatalogSnapshot.EngineGeneration++ }},
		{name: "object key", mutate: func(view *View) {
			view.Objects[0].Key.NormalizedAPIName = "contact"
			view.RecordTypes[0].ObjectKey.NormalizedAPIName = "contact"
		}},
		{name: "object target", mutate: func(view *View) { view.Objects[0].ResolvedObjectID = "obj-other" }},
		{name: "object state", mutate: func(view *View) {
			view.Objects[0].State = IdentityStateTombstone
			view.RecordTypes[0].State = IdentityStateTombstone
		}},
		{name: "record-type namespace", mutate: func(view *View) {
			view.RecordTypes[0].Key = RecordTypeKey{NamespaceID: "io.pingcap.sales", LocalKey: "record-type.enterprise"}
		}},
		{name: "record-type local key", mutate: func(view *View) {
			view.RecordTypes[0].Key = RecordTypeKey{NamespaceID: SystemNamespaceID, LocalKey: "record-type.standard"}
		}},
		{name: "record-type target", mutate: func(view *View) { view.RecordTypes[0].ResolvedRecordTypeID = "rt-other" }},
		{name: "record-type state", mutate: func(view *View) { view.RecordTypes[0].State = IdentityStateTombstone }},
		{name: "product App key", mutate: func(view *View) { view.ProductApps[0].Key.AppID = "io.pingcap.service" }},
		{name: "product App target", mutate: func(view *View) {
			view.ProductApps[0].ResolvedTarget.ResolvedAppInstallationID = testInstallationID2
		}},
		{name: "product App state", mutate: func(view *View) { view.ProductApps[0].State = IdentityStateTombstone }},
		{name: "navigation namespace", mutate: func(view *View) { view.NavigationApps[0].Key.NamespaceID = "io.pingcap.service" }},
		{name: "navigation local key", mutate: func(view *View) { view.NavigationApps[0].Key.LocalKey = "navigation.service" }},
		{name: "navigation target", mutate: func(view *View) { view.NavigationApps[0].ResolvedAppConfigID = "nav-other" }},
		{name: "navigation state", mutate: func(view *View) { view.NavigationApps[0].State = IdentityStateTombstone }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			view := cloneAssignmentView(base)
			test.mutate(&view)
			if err := ValidateView(view); err != nil {
				t.Fatalf("mutated view is not valid: %v", err)
			}
			got, err := ViewDigest(view)
			if err != nil {
				t.Fatal(err)
			}
			if got == want {
				t.Fatalf("digest did not change from %s", want)
			}
		})
	}
}

func TestUnsignedLEB128Helpers(t *testing.T) {
	tests := []struct {
		value uint64
		want  []byte
	}{
		{value: 0, want: []byte{0x00}},
		{value: 1, want: []byte{0x01}},
		{value: 127, want: []byte{0x7f}},
		{value: 128, want: []byte{0x80, 0x01}},
		{value: 16_383, want: []byte{0xff, 0x7f}},
		{value: 16_384, want: []byte{0x80, 0x80, 0x01}},
		{value: 624_485, want: []byte{0xe5, 0x8e, 0x26}},
		{value: math.MaxUint64, want: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01}},
	}
	for _, test := range tests {
		var out bytes.Buffer
		writeUint(&out, test.value)
		if got := out.Bytes(); !bytes.Equal(got, test.want) {
			t.Errorf("writeUint(%d) = %x, want %x", test.value, got, test.want)
		}
		if got := uintSize(test.value); got != uint64(len(test.want)) {
			t.Errorf("uintSize(%d) = %d, want %d", test.value, got, len(test.want))
		}
	}
}

func TestCanonicalViewSizeLimitHelper(t *testing.T) {
	view := validAssignmentView()
	canonical, err := CanonicalViewBytes(view)
	if err != nil {
		t.Fatal(err)
	}
	size, ok := canonicalViewSize(view)
	if !ok {
		t.Fatal("canonicalViewSize unexpectedly overflowed")
	}
	if size != uint64(len(canonical)) {
		t.Fatalf("canonicalViewSize = %d, encoded length = %d", size, len(canonical))
	}
	if err := validateCanonicalViewSize(view, size); err != nil {
		t.Fatalf("exact size limit was rejected: %v", err)
	}
	if size == 0 {
		t.Fatal("canonical view unexpectedly has zero size")
	}
	expectErrorContains(t, validateCanonicalViewSize(view, size-1), "canonical bytes exceed")
}

func TestCanonicalProductAppTargetSizeIsTaggedAndConditional(t *testing.T) {
	withHistory := validAssignmentView()
	withHistory.ProductApps[0].State = IdentityStateTombstone
	neverSelected := cloneAssignmentView(withHistory)
	neverSelected.ProductApps[0].ResolvedTarget = absentProductAppTarget()

	withHistoryBytes, err := CanonicalViewBytes(withHistory)
	if err != nil {
		t.Fatal(err)
	}
	neverSelectedBytes, err := CanonicalViewBytes(neverSelected)
	if err != nil {
		t.Fatal(err)
	}
	withHistorySize, ok := canonicalViewSize(withHistory)
	if !ok {
		t.Fatal("present target size overflowed")
	}
	neverSelectedSize, ok := canonicalViewSize(neverSelected)
	if !ok {
		t.Fatal("absent target size overflowed")
	}
	if withHistorySize != uint64(len(withHistoryBytes)) || neverSelectedSize != uint64(len(neverSelectedBytes)) {
		t.Fatalf("size accounting disagrees with encoding: present=%d/%d absent=%d/%d",
			withHistorySize, len(withHistoryBytes), neverSelectedSize, len(neverSelectedBytes))
	}
	if difference := withHistorySize - neverSelectedSize; difference != stringSize(testInstallationID1) {
		t.Fatalf("present target adds %d bytes, want conditional S(id) size %d", difference, stringSize(testInstallationID1))
	}

	if _, ok := checkedAddSize(math.MaxUint64, 1); ok {
		t.Fatal("checkedAddSize accepted uint64 overflow")
	}
	if got, ok := checkedAddSize(math.MaxUint64-1, 1); !ok || got != math.MaxUint64 {
		t.Fatalf("checkedAddSize exact boundary = (%d, %t), want (%d, true)", got, ok, uint64(math.MaxUint64))
	}
}

func validAssignmentView() View {
	return View{
		SourceCatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
			OrgID:                 "org-1",
			CatalogRevision:       7,
			CatalogSnapshotDigest: testDigest("a"),
			SchemaGeneration:      11,
			EngineGeneration:      13,
		},
		CanonicalizationVersion:       CanonicalizationVersion,
		KeyGrammarVersion:             KeyGrammarVersion,
		ObjectKeyNormalizationVersion: ObjectKeyNormalizationVersion,
		Objects: []ObjectIdentity{{
			Key: ObjectKey{NormalizedAPIName: "account"}, ResolvedObjectID: "obj-account", State: IdentityStateActive,
		}},
		RecordTypes: []RecordTypeIdentity{{
			ObjectKey:            ObjectKey{NormalizedAPIName: "account"},
			Key:                  RecordTypeKey{NamespaceID: SystemNamespaceID, LocalKey: MasterRecordTypeLocalKey},
			ResolvedRecordTypeID: "rt-master", State: IdentityStateActive,
		}},
		ProductApps: []ProductAppIdentity{{
			Key: ProductAppKey{AppID: "io.pingcap.sales"}, State: IdentityStateActive,
			ResolvedTarget: presentProductAppTarget(testInstallationID1),
		}},
		NavigationApps: []NavigationAppIdentity{{
			Key: NavigationAppKey{NamespaceID: "io.pingcap.sales", LocalKey: "navigation.sales"}, ResolvedAppConfigID: "nav-sales", State: IdentityStateActive,
		}},
	}
}

func multiEntryAssignmentView() View {
	view := validAssignmentView()
	view.Objects = append([]ObjectIdentity{{
		Key: ObjectKey{NormalizedAPIName: "contact"}, ResolvedObjectID: "obj-contact", State: IdentityStateTombstone,
	}}, view.Objects...)
	view.RecordTypes = append([]RecordTypeIdentity{{
		ObjectKey:            ObjectKey{NormalizedAPIName: "contact"},
		Key:                  RecordTypeKey{NamespaceID: "io.pingcap.sales", LocalKey: "record-type.enterprise"},
		ResolvedRecordTypeID: "rt-enterprise", State: IdentityStateTombstone,
	}}, view.RecordTypes...)
	view.ProductApps = append(view.ProductApps, ProductAppIdentity{
		Key: ProductAppKey{AppID: "io.pingcap.alpha"}, State: IdentityStateTombstone,
		ResolvedTarget: presentProductAppTarget(testInstallationID2),
	})
	view.NavigationApps = append(view.NavigationApps, NavigationAppIdentity{
		Key: NavigationAppKey{NamespaceID: "io.pingcap.alpha", LocalKey: "navigation.home"}, ResolvedAppConfigID: "nav-home", State: IdentityStateTombstone,
	})
	return view
}

func cloneAssignmentView(view View) View {
	cloned := view
	cloned.Objects = append([]ObjectIdentity{}, view.Objects...)
	cloned.RecordTypes = append([]RecordTypeIdentity{}, view.RecordTypes...)
	cloned.ProductApps = append([]ProductAppIdentity{}, view.ProductApps...)
	cloned.NavigationApps = append([]NavigationAppIdentity{}, view.NavigationApps...)
	return cloned
}

func reverseAssignmentView(view View) View {
	reversed := cloneAssignmentView(view)
	for left, right := 0, len(reversed.Objects)-1; left < right; left, right = left+1, right-1 {
		reversed.Objects[left], reversed.Objects[right] = reversed.Objects[right], reversed.Objects[left]
	}
	for left, right := 0, len(reversed.RecordTypes)-1; left < right; left, right = left+1, right-1 {
		reversed.RecordTypes[left], reversed.RecordTypes[right] = reversed.RecordTypes[right], reversed.RecordTypes[left]
	}
	for left, right := 0, len(reversed.ProductApps)-1; left < right; left, right = left+1, right-1 {
		reversed.ProductApps[left], reversed.ProductApps[right] = reversed.ProductApps[right], reversed.ProductApps[left]
	}
	for left, right := 0, len(reversed.NavigationApps)-1; left < right; left, right = left+1, right-1 {
		reversed.NavigationApps[left], reversed.NavigationApps[right] = reversed.NavigationApps[right], reversed.NavigationApps[left]
	}
	return reversed
}

func assertObjectOrder(t *testing.T, values []ObjectIdentity, want []string) {
	t.Helper()
	got := make([]string, len(values))
	for index, value := range values {
		got[index] = value.Key.NormalizedAPIName
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("object order = %q, want %q", got, want)
	}
}

func assertRecordTypeOrder(t *testing.T, values []RecordTypeIdentity, want []string) {
	t.Helper()
	got := make([]string, len(values))
	for index, value := range values {
		got[index] = recordTypeIdentity(value)
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("record-type order = %q, want %q", got, want)
	}
}

func assertProductAppOrder(t *testing.T, values []ProductAppIdentity, want []string) {
	t.Helper()
	got := make([]string, len(values))
	for index, value := range values {
		got[index] = value.Key.AppID
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("product App order = %q, want %q", got, want)
	}
}

func assertNavigationAppOrder(t *testing.T, values []NavigationAppIdentity, want []string) {
	t.Helper()
	got := make([]string, len(values))
	for index, value := range values {
		got[index] = namespacedKeyIdentity(value.Key.NamespaceID, value.Key.LocalKey)
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("navigation App order = %q, want %q", got, want)
	}
}

func expectErrorContains(t *testing.T, err error, want string) {
	t.Helper()
	if err == nil {
		t.Fatalf("expected error containing %q, got nil", want)
	}
	if !strings.Contains(err.Error(), want) {
		t.Fatalf("error %q does not contain %q", err, want)
	}
}

func testDigest(hexDigit string) string {
	return "sha256:" + strings.Repeat(hexDigit, 64)
}

func presentProductAppTarget(installationID string) ProductAppResolvedTarget {
	return ProductAppResolvedTarget{
		Kind:                      ProductAppResolvedTargetKindAppInstallationID,
		ResolvedAppInstallationID: installationID,
	}
}

func absentProductAppTarget() ProductAppResolvedTarget {
	return ProductAppResolvedTarget{Kind: ProductAppResolvedTargetKindAbsent}
}
