package permissionauthoringv1

import (
	"context"
	"encoding/hex"
	"errors"
	"testing"
	"time"
)

func TestRequestIsOneAtomicCanonicalPatch(t *testing.T) {
	request := validRequest()
	request.Changes.System = []CapabilityGrantChange{{CapabilityID: "Zed", Enabled: true}, {CapabilityID: "Alpha", Enabled: false}}
	request.Changes.Fields = []FieldGrantChange{
		{ResourceType: "Contact", FieldName: "Name", Read: true},
		{ResourceType: "Account", FieldName: "Phone", Read: true, Edit: true},
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest(): %v", err)
	}

	digest, err := CommandDigest(request)
	if err != nil {
		t.Fatalf("CommandDigest(): %v", err)
	}
	reordered := request
	reordered.Changes.System = []CapabilityGrantChange{{CapabilityID: "Alpha", Enabled: false}, {CapabilityID: "Zed", Enabled: true}}
	reordered.Changes.Fields = []FieldGrantChange{request.Changes.Fields[1], request.Changes.Fields[0]}
	if other, err := CommandDigest(reordered); err != nil || other != digest {
		t.Fatalf("reordered digest = %q, %v; want %q", other, err, digest)
	}

	higherCatalog := uint64(99)
	reordered.MinCatalogRevision = &higherCatalog
	if other, err := CommandDigest(reordered); err != nil || other != digest {
		t.Fatalf("retry-fence digest = %q, %v; want %q", other, err, digest)
	}
	reordered.CommandID = "permission-save:2"
	if other, err := CommandDigest(reordered); err != nil || other != digest {
		t.Fatalf("lookup-key digest = %q, %v; want %q", other, err, digest)
	}

	reordered.ExpectedAuthoringToken = tokenForTest(t, "permission-set-1", 2)
	if other, err := CommandDigest(reordered); err != nil || other == digest {
		t.Fatalf("changed authoring token digest = %q, %v; want a different digest", other, err)
	}
}

func TestRecordTypeObjectGroupsBindProfileCommandsToCurrentObjectMembership(t *testing.T) {
	catalog := []RecordType{
		{RecordTypeID: "account-a", ResourceType: "Account", StableName: "AccountA", Label: "Account A", Active: true},
		{RecordTypeID: "account-b", ResourceType: "Account", StableName: "AccountB", Label: "Account B", Active: true},
		{RecordTypeID: "account-old", ResourceType: "Account", StableName: "AccountOld", Label: "Account Old", Active: false},
		{RecordTypeID: "contact-a", ResourceType: "Contact", StableName: "ContactA", Label: "Contact A", Active: true},
	}
	tests := []struct {
		name    string
		target  TargetKind
		changes []RecordTypeGrantChange
		want    RecordTypeObjectGroupDisposition
	}{
		{name: "complete Profile object", target: TargetProfile, changes: []RecordTypeGrantChange{
			{RecordTypeID: "account-a", ResourceType: "Account", Visible: false},
			{RecordTypeID: "account-b", ResourceType: "Account", Visible: true},
		}, want: RecordTypeObjectGroupComplete},
		{name: "two complete Profile objects", target: TargetProfile, changes: []RecordTypeGrantChange{
			{RecordTypeID: "account-a", ResourceType: "Account", Visible: false},
			{RecordTypeID: "account-b", ResourceType: "Account", Visible: true},
			{RecordTypeID: "contact-a", ResourceType: "Contact", Visible: true},
		}, want: RecordTypeObjectGroupComplete},
		{name: "incomplete Profile object", target: TargetProfile, changes: []RecordTypeGrantChange{
			{RecordTypeID: "account-a", ResourceType: "Account", Visible: false},
		}, want: RecordTypeObjectGroupIncomplete},
		{name: "ordinary PermissionSet stays sparse", target: TargetPermissionSet, changes: []RecordTypeGrantChange{
			{RecordTypeID: "account-a", ResourceType: "Account", Visible: true},
		}, want: RecordTypeObjectGroupComplete},
		{name: "reparented identity", target: TargetProfile, changes: []RecordTypeGrantChange{
			{RecordTypeID: "contact-a", ResourceType: "Account", Visible: true},
		}, want: RecordTypeObjectGroupMembershipChanged},
		{name: "inactive identity", target: TargetProfile, changes: []RecordTypeGrantChange{
			{RecordTypeID: "account-old", ResourceType: "Account", Visible: false},
		}, want: RecordTypeObjectGroupUnknownOrInactive},
		{name: "unknown identity", target: TargetPermissionSet, changes: []RecordTypeGrantChange{
			{RecordTypeID: "missing", ResourceType: "Account", Visible: true},
		}, want: RecordTypeObjectGroupUnknownOrInactive},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := ClassifyRecordTypeObjectGroups(
				Target{Kind: test.target, ID: "target-1"}, catalog, test.changes,
			)
			if got != test.want {
				t.Fatalf("ClassifyRecordTypeObjectGroups() = %q, want %q", got, test.want)
			}
		})
	}

	duplicateCatalog := append(append([]RecordType(nil), catalog...), catalog[0])
	changes := []RecordTypeGrantChange{
		{RecordTypeID: "account-a", ResourceType: "Account"},
		{RecordTypeID: "account-b", ResourceType: "Account"},
	}
	if got := ClassifyRecordTypeObjectGroups(
		Target{Kind: TargetProfile, ID: "profile-1"}, duplicateCatalog, changes,
	); got != RecordTypeObjectGroupInvalidCatalog {
		t.Fatalf("duplicate Catalog disposition = %q", got)
	}
	malformedCatalog := append([]RecordType(nil), catalog...)
	malformedCatalog[0].StableName = ""
	if got := ClassifyRecordTypeObjectGroups(
		Target{Kind: TargetProfile, ID: "profile-1"}, malformedCatalog, changes,
	); got != RecordTypeObjectGroupInvalidCatalog {
		t.Fatalf("malformed Catalog disposition = %q", got)
	}
	noncanonicalCatalog := append([]RecordType(nil), catalog...)
	noncanonicalCatalog[0], noncanonicalCatalog[1] = noncanonicalCatalog[1], noncanonicalCatalog[0]
	if got := ClassifyRecordTypeObjectGroups(
		Target{Kind: TargetProfile, ID: "profile-1"}, noncanonicalCatalog, changes,
	); got != RecordTypeObjectGroupInvalidCatalog {
		t.Fatalf("noncanonical Catalog disposition = %q", got)
	}
}

func TestRecordTypeObjectGroupCompatibilityFenceIsVersioned(t *testing.T) {
	if CapabilityName != "runtime.permissions.authoring.v1" ||
		RecordTypeObjectGroupsCapabilityName != "runtime.permissions.authoring.record-type-object-groups.v1" {
		t.Fatalf("permission authoring capabilities = %q, %q", CapabilityName, RecordTypeObjectGroupsCapabilityName)
	}
	if SemanticMappingRevision != 3 {
		t.Fatalf("SemanticMappingRevision = %d, want 3", SemanticMappingRevision)
	}
}

func TestRequestRejectsElapsedDeadlineBeforeCancellationPropagation(t *testing.T) {
	ctx := elapsedPermissionAuthoringContext{Context: context.Background()}
	var failure *Failure
	if err := ValidateRequest(ctx, validRequest()); !errors.As(err, &failure) ||
		failure.Category != FailureInvalidRequest || failure.InvalidComponent != InvalidComponentDeadline {
		t.Fatalf("ValidateRequest(elapsed deadline) = %#v", err)
	}
	if err := ValidateStatusRequest(ctx, StatusRequest{}); !errors.As(err, &failure) || failure.Category != FailureInvalidRequest || failure.InvalidComponent != InvalidComponentDeadline {
		t.Fatalf("ValidateStatusRequest(elapsed deadline) = %#v", err)
	}
	if err := ValidateSnapshotRequest(ctx, validSnapshotRequest()); !errors.As(err, &failure) || failure.Category != FailureInvalidRequest || failure.InvalidComponent != InvalidComponentDeadline {
		t.Fatalf("ValidateSnapshotRequest(elapsed deadline) = %#v", err)
	}
}

type elapsedPermissionAuthoringContext struct {
	context.Context
}

func (elapsedPermissionAuthoringContext) Deadline() (time.Time, bool) {
	return time.Now().Add(-time.Second), true
}

func TestRequestRejectsIndependentOrAmbiguousFamilyShapes(t *testing.T) {
	tests := map[string]func(*Request){
		"empty aggregate": func(request *Request) { request.Changes = Changes{} },
		"duplicate system": func(request *Request) {
			request.Changes.System = []CapabilityGrantChange{{CapabilityID: "ManageUsers"}, {CapabilityID: "ManageUsers", Enabled: true}}
		},
		"duplicate object folded": func(request *Request) {
			request.Changes.Objects = []ObjectGrantChange{{ResourceType: "Account", Read: true}, {ResourceType: "account", Read: true}}
		},
		"field edit without read": func(request *Request) {
			request.Changes.Fields = []FieldGrantChange{{ResourceType: "Account", FieldName: "Name", Edit: true}}
		},
		"object write without read": func(request *Request) {
			request.Changes.Objects = []ObjectGrantChange{{ResourceType: "Account", Edit: true}}
		},
		"zero catalog fence": func(request *Request) { zero := uint64(0); request.MinCatalogRevision = &zero },
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			request := validRequest()
			mutate(&request)
			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()
			var failure *Failure
			if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != FailureInvalidRequest {
				t.Fatalf("ValidateRequest() = %v, want INVALID_REQUEST", err)
			}
		})
	}
}

func TestAuthoringTokenBindsCanonicalPermissionSetIdentityAndRevision(t *testing.T) {
	token := tokenForTest(t, "permission-set-1", 7)
	if !AuthoringTokenMatches(token, "permission-set-1", 7) {
		t.Fatal("canonical authoring token did not match its aggregate")
	}
	if AuthoringTokenMatches(token, "permission-set-2", 7) {
		t.Fatal("equal revision on another PermissionSet reused the authoring token")
	}
	if AuthoringTokenMatches(token, "permission-set-1", 8) {
		t.Fatal("later revision reused the prior authoring token")
	}
	if tokenForTest(t, "permission-set-2", 7) == token {
		t.Fatal("token representation does not include canonical aggregate identity")
	}
}

func TestAuthoringTokenRejectsNonCanonicalRevisionSpellingEverywhere(t *testing.T) {
	nonCanonical := nonCanonicalTokenForTest("permission-set-1", "+1")
	request := validRequest()
	request.ExpectedAuthoringToken = nonCanonical
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateRequest(ctx, request); err == nil {
		t.Fatal("ValidateRequest accepted a leading-plus authoring revision")
	}

	snapshotRequest := validSnapshotRequest()
	snapshot := validSnapshotResponse(t)
	snapshot.AuthoringToken = nonCanonical
	if err := ValidateSnapshotResponse(snapshotRequest, snapshot); err == nil {
		t.Fatal("ValidateSnapshotResponse accepted a leading-plus authoring revision")
	}

	valid := validRequest()
	digest, err := CommandDigest(valid)
	if err != nil {
		t.Fatal(err)
	}
	response := Response{
		CommandID: valid.CommandID, CommandDigest: digest, Outcome: OutcomeApplied,
		CanonicalPermissionSetID: "permission-set-1", AuthoringToken: nonCanonical,
		Receipts: validReceiptsForTest(), OutcomeExpiresAtUTC: "2026-08-31T00:00:00Z",
	}
	if err := ValidateResponse(valid, response); err == nil {
		t.Fatal("ValidateResponse accepted a leading-plus authoring revision")
	}
}

func TestSnapshotIsOneCanonicalFiveFamilyRead(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := validSnapshotRequest()
	response := validSnapshotResponse(t)
	if err := ValidateSnapshotRequest(ctx, request); err != nil {
		t.Fatalf("ValidateSnapshotRequest(): %v", err)
	}
	if err := ValidateSnapshotResponse(request, response); err != nil {
		t.Fatalf("ValidateSnapshotResponse(): %v", err)
	}

	wrongIdentity := response
	wrongIdentity.CanonicalPermissionSetID = "permission-set-2"
	if err := ValidateSnapshotResponse(request, wrongIdentity); err == nil {
		t.Fatal("snapshot accepted a token bound to another PermissionSet")
	}
	directRequest := request
	directRequest.Target = Target{Kind: TargetPermissionSet, ID: "permission-set-1"}
	wrongDirectAggregate := response
	wrongDirectAggregate.CanonicalPermissionSetID = "permission-set-2"
	wrongDirectAggregate.AuthoringToken = tokenForTest(t, "permission-set-2", 1)
	if err := ValidateSnapshotResponse(directRequest, wrongDirectAggregate); err == nil {
		t.Fatal("snapshot accepted another self-consistent aggregate for a direct PermissionSet target")
	}

	partialOrder := response
	partialOrder.Grants.System = []CapabilityGrant{{CapabilityID: "Zed"}, {CapabilityID: "Alpha"}}
	if err := ValidateSnapshotResponse(request, partialOrder); err == nil {
		t.Fatal("snapshot accepted a non-canonical family order")
	}
	normalized := NormalizeSnapshotResponse(partialOrder)
	if err := ValidateSnapshotResponse(request, normalized); err != nil {
		t.Fatalf("normalized snapshot: %v", err)
	}
	if normalized.Grants.System[0].CapabilityID != "Alpha" || response.Grants.System[0].CapabilityID != "ManageUsers" {
		t.Fatal("snapshot normalization did not deep-copy and sort")
	}
}

func TestSnapshotRecordTypeGrantsAreCatalogAndTargetBound(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*SnapshotRequest, *SnapshotResponse)
		valid  bool
	}{
		{
			name: "Profile explicit false row",
			mutate: func(_ *SnapshotRequest, response *SnapshotResponse) {
				response.Grants.RecordTypes[0].Visible = false
			},
			valid: true,
		},
		{
			name: "Profile snapshot remains sparse",
			mutate: func(_ *SnapshotRequest, response *SnapshotResponse) {
				response.Catalog.RecordTypes = append(response.Catalog.RecordTypes, RecordType{
					RecordTypeID: "record-type-2", ResourceType: "Account", StableName: "Consumer", Label: "Consumer", Active: true,
				})
			},
			valid: true,
		},
		{
			name: "unknown stored row",
			mutate: func(_ *SnapshotRequest, response *SnapshotResponse) {
				response.Grants.RecordTypes[0].RecordTypeID = "record-type-missing"
			},
		},
		{
			name: "inactive stored row",
			mutate: func(_ *SnapshotRequest, response *SnapshotResponse) {
				response.Catalog.RecordTypes[0].Active = false
			},
		},
		{
			name: "ordinary PermissionSet true row",
			mutate: func(request *SnapshotRequest, _ *SnapshotResponse) {
				request.Target = Target{Kind: TargetPermissionSet, ID: "permission-set-1"}
			},
			valid: true,
		},
		{
			name: "ordinary PermissionSet physical false row remains observable",
			mutate: func(request *SnapshotRequest, response *SnapshotResponse) {
				request.Target = Target{Kind: TargetPermissionSet, ID: "permission-set-1"}
				response.Grants.RecordTypes[0].Visible = false
			},
			valid: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			request := validSnapshotRequest()
			response := validSnapshotResponse(t)
			test.mutate(&request, &response)
			err := ValidateSnapshotResponse(request, response)
			if test.valid && err != nil {
				t.Fatalf("ValidateSnapshotResponse() = %v, want valid", err)
			}
			if !test.valid && err == nil {
				t.Fatal("ValidateSnapshotResponse() accepted invalid RecordType grants")
			}
		})
	}
}

func TestSnapshotCatalogTextUsesFieldSpecificControlPolicy(t *testing.T) {
	mutations := map[string]func(*SnapshotResponse, string){
		"system label": func(response *SnapshotResponse, value string) {
			response.Catalog.System[0].Label = value
		},
		"system category": func(response *SnapshotResponse, value string) {
			response.Catalog.System[0].Category = value
		},
		"record type label": func(response *SnapshotResponse, value string) {
			response.Catalog.RecordTypes[0].Label = value
		},
		"custom permission label": func(response *SnapshotResponse, value string) {
			response.Catalog.Custom[0].Label = value
		},
	}
	for name, mutate := range mutations {
		for controlName, control := range map[string]string{"LF": "\n", "CR": "\r", "TAB": "\t"} {
			t.Run(name+"/"+controlName, func(t *testing.T) {
				request := validSnapshotRequest()
				response := validSnapshotResponse(t)
				mutate(&response, "left"+control+"right")
				if err := ValidateSnapshotResponse(request, response); err == nil {
					t.Fatal("ValidateSnapshotResponse accepted a control character in single-line catalog text")
				}
			})
		}
	}

	request := validSnapshotRequest()
	response := validSnapshotResponse(t)
	response.Catalog.System[0].Description = "first line\nsecond line\r\n\tcontinued"
	if err := ValidateSnapshotResponse(request, response); err != nil {
		t.Fatalf("ValidateSnapshotResponse rejected allowed description line breaks: %v", err)
	}
}

func TestResponseAndStatusBindExactTerminalOutcome(t *testing.T) {
	request := validRequest()
	digest, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	response := Response{
		CommandID: request.CommandID, CommandDigest: digest, Outcome: OutcomeApplied,
		CanonicalPermissionSetID: "permission-set-1", AuthoringToken: tokenForTest(t, "permission-set-1", 2), Receipts: validReceiptsForTest(),
		OutcomeExpiresAtUTC: "2026-08-31T00:00:00Z",
	}
	if err := ValidateResponse(request, response); err != nil {
		t.Fatalf("ValidateResponse(): %v", err)
	}
	missingToken := response
	missingToken.AuthoringToken = ""
	if err := ValidateResponse(request, missingToken); err == nil {
		t.Fatal("ValidateResponse accepted a successful outcome without the next authoring token")
	}
	statusRequest := StatusRequest{CommandID: request.CommandID, CommandDigest: digest}
	if err := ValidateStatusResponse(statusRequest, StatusResponse{
		CommandID: request.CommandID, CommandDigest: digest, State: CommandCompleted, Result: &response,
	}); err != nil {
		t.Fatalf("ValidateStatusResponse(): %v", err)
	}
	if err := ValidateStatusResponseForRequest(request, StatusResponse{
		CommandID: request.CommandID, CommandDigest: digest, State: CommandCompleted, Result: &response,
	}); err != nil {
		t.Fatalf("ValidateStatusResponseForRequest(): %v", err)
	}
	if err := ValidateStatusResponse(StatusRequest{}, StatusResponse{State: CommandAccepted}); err == nil {
		t.Fatal("ValidateStatusResponse accepted malformed command identity")
	}

	rejected := response
	rejected.Outcome = OutcomeRejected
	rejected.RejectionCode = RejectionStaleAuthoringState
	rejected.CanonicalPermissionSetID = ""
	rejected.AuthoringToken = ""
	if err := ValidateResponse(request, rejected); err != nil {
		t.Fatalf("ValidateResponse(rejected): %v", err)
	}

	rejected.AuthoringToken = "leaked-current-token"
	if err := ValidateResponse(request, rejected); err == nil {
		t.Fatal("ValidateResponse accepted a token on a rejected outcome")
	}
}

func TestTerminalTransitionBindsRequestBaseline(t *testing.T) {
	tests := []struct {
		name             string
		target           Target
		expectedRevision uint64
		outcome          Outcome
		responseID       string
		responseRevision uint64
		wantValid        bool
	}{
		{
			name: "Profile applied advances exactly once", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: 1, outcome: OutcomeApplied, responseID: "permission-set-1", responseRevision: 2, wantValid: true,
		},
		{
			name: "Profile noop preserves exact token", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: 1, outcome: OutcomeNoop, responseID: "permission-set-1", responseRevision: 1, wantValid: true,
		},
		{
			name: "Profile cannot substitute aggregate", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: 1, outcome: OutcomeApplied, responseID: "permission-set-2", responseRevision: 2,
		},
		{
			name: "direct PermissionSet cannot substitute aggregate", target: Target{Kind: TargetPermissionSet, ID: "permission-set-1"},
			expectedRevision: 1, outcome: OutcomeApplied, responseID: "permission-set-2", responseRevision: 2,
		},
		{
			name: "applied cannot preserve revision", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: 1, outcome: OutcomeApplied, responseID: "permission-set-1", responseRevision: 1,
		},
		{
			name: "applied cannot skip revisions", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: 1, outcome: OutcomeApplied, responseID: "permission-set-1", responseRevision: 99,
		},
		{
			name: "noop cannot advance revision", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: 1, outcome: OutcomeNoop, responseID: "permission-set-1", responseRevision: 2,
		},
		{
			name: "applied may reach maximum revision", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: ^uint64(0) - 1, outcome: OutcomeApplied, responseID: "permission-set-1", responseRevision: ^uint64(0), wantValid: true,
		},
		{
			name: "applied cannot overflow revision", target: Target{Kind: TargetProfile, ID: "profile-1"},
			expectedRevision: ^uint64(0), outcome: OutcomeApplied, responseID: "permission-set-1", responseRevision: ^uint64(0),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			request := validRequest()
			request.Target = test.target
			request.ExpectedAuthoringToken = tokenForTest(t, "permission-set-1", test.expectedRevision)
			digest, err := CommandDigest(request)
			if err != nil {
				t.Fatal(err)
			}
			response := Response{
				CommandID: request.CommandID, CommandDigest: digest, Outcome: test.outcome,
				CanonicalPermissionSetID: test.responseID,
				AuthoringToken:           tokenForTest(t, test.responseID, test.responseRevision),
				Receipts:                 validReceiptsForTest(),
				OutcomeExpiresAtUTC:      "2026-08-31T00:00:00Z",
			}
			if gotValid := ValidateResponse(request, response) == nil; gotValid != test.wantValid {
				t.Fatalf("ValidateResponse() valid = %t, want %t", gotValid, test.wantValid)
			}

			statusRequest, err := StatusRequestFor(request)
			if err != nil {
				t.Fatal(err)
			}
			status := StatusResponse{
				CommandID: request.CommandID, CommandDigest: digest,
				State: CommandCompleted, Result: &response,
			}
			if err := ValidateStatusResponse(statusRequest, status); err != nil {
				t.Fatalf("request-independent Status shape rejected a well-formed terminal: %v", err)
			}
			if gotValid := ValidateStatusResponseForRequest(request, status) == nil; gotValid != test.wantValid {
				t.Fatalf("ValidateStatusResponseForRequest() valid = %t, want %t", gotValid, test.wantValid)
			}
		})
	}
}

func TestCatalogFenceAppliesToFreshAdmissionNotTerminalReplay(t *testing.T) {
	request := validRequest()
	digest, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	response := Response{
		CommandID: request.CommandID, CommandDigest: digest, Outcome: OutcomeApplied,
		CanonicalPermissionSetID: "permission-set-1", AuthoringToken: tokenForTest(t, "permission-set-1", 2), Receipts: validReceiptsForTest(),
		OutcomeExpiresAtUTC: "2026-08-31T00:00:00Z",
	}
	response.Receipts.CatalogRevision = 6
	if MeetsFreshCommandCatalogFence(request, response.Receipts.CatalogRevision) {
		t.Fatal("fresh admission accepted Catalog revision below MinCatalogRevision")
	}
	if err := ValidateResponse(request, response); err != nil {
		t.Fatalf("terminal replay was invalidated by a later admission fence: %v", err)
	}
}

func TestTopLevelFailuresDoNotRepresentAcceptedBusinessRejections(t *testing.T) {
	if !ValidSaveTopLevelFailure(FailureForbidden) {
		t.Fatal("complete-call authorization denial is not representable")
	}
	if ValidSaveTopLevelFailure(commonMissingForTest) {
		t.Fatal("accepted target rejection escaped the durable terminal outcome")
	}
	if !ValidStatusTopLevelFailure(FailureMissing) {
		t.Fatal("status must permit indistinguishable unknown/expired command MISSING")
	}
	if got := RetryDispositionFor(&Failure{Category: FailureStaleState}); got != RetryAfterStateRefresh {
		t.Fatalf("stale catalog retry = %q, want state refresh", got)
	}
}

const (
	commonMissingForTest FailureCategory = "MISSING"
)

func validRequest() Request {
	revision := uint64(7)
	return Request{
		CommandID:              "permission-save:1",
		Target:                 Target{Kind: TargetProfile, ID: "profile-1"},
		ExpectedAuthoringToken: mustAuthoringTokenForTest("permission-set-1", 1),
		Changes: Changes{
			System:      []CapabilityGrantChange{{CapabilityID: "ManageUsers", Enabled: true}},
			Objects:     []ObjectGrantChange{{ResourceType: "Account", Read: true, Edit: true}},
			Fields:      []FieldGrantChange{{ResourceType: "Account", FieldName: "Name", Read: true, Edit: true}},
			RecordTypes: []RecordTypeGrantChange{{RecordTypeID: "record-type-1", ResourceType: "Account", Visible: true}},
			Custom:      []CapabilityGrantChange{{CapabilityID: "custom-permission-1", Enabled: true}},
		},
		MinCatalogRevision: &revision,
	}
}

func validSnapshotRequest() SnapshotRequest {
	revision := uint64(7)
	return SnapshotRequest{
		Target:             Target{Kind: TargetProfile, ID: "profile-1"},
		MinCatalogRevision: &revision,
	}
}

func validSnapshotResponse(t *testing.T) SnapshotResponse {
	t.Helper()
	return SnapshotResponse{
		CanonicalPermissionSetID: "permission-set-1",
		AuthoringToken:           tokenForTest(t, "permission-set-1", 7),
		Catalog: SnapshotCatalog{
			System:      []SystemPermission{{PermissionID: "ManageUsers", Label: "Manage Users", Category: "Setup"}},
			RecordTypes: []RecordType{{RecordTypeID: "record-type-1", ResourceType: "Account", StableName: "Business", Label: "Business", Active: true}},
			Custom:      []CustomPermission{{PermissionID: "custom-permission-1", StableName: "CanExport", Label: "Can Export"}},
		},
		Grants: StoredGrants{
			System:      []CapabilityGrant{{CapabilityID: "ManageUsers", Enabled: true}},
			Objects:     []ObjectGrant{{ResourceType: "Account", Read: true, Edit: true}},
			Fields:      []FieldGrant{{ResourceType: "Account", FieldName: "Name", Read: true, Edit: true}},
			RecordTypes: []RecordTypeGrant{{RecordTypeID: "record-type-1", Visible: true}},
			Custom:      []CapabilityGrant{{CapabilityID: "custom-permission-1", Enabled: true}},
		},
		Receipts: validReceiptsForTest(),
	}
}

func tokenForTest(t *testing.T, permissionSetID string, revision uint64) string {
	t.Helper()
	token, err := NewAuthoringToken(permissionSetID, revision)
	if err != nil {
		t.Fatal(err)
	}
	return token
}

func nonCanonicalTokenForTest(permissionSetID, revisionText string) string {
	digest := authoringTokenDigest(permissionSetID, revisionText)
	return authoringTokenPrefix + revisionText + ":" + hex.EncodeToString(digest[:])
}

func mustAuthoringTokenForTest(permissionSetID string, revision uint64) string {
	token, err := NewAuthoringToken(permissionSetID, revision)
	if err != nil {
		panic(err)
	}
	return token
}

func validReceiptsForTest() Receipts {
	return Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: 7, SecurityEpoch: 9}
}
