package catalogverificationtx

import (
	"context"
	"encoding/json"
	"errors"
	"reflect"
	"strings"
	"testing"

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

func TestLeaseSharesCanonicalOrderAcrossAppAndTenantClaims(t *testing.T) {
	t.Run("App then tenant", func(t *testing.T) {
		db, mock, token := newVerificationToken(t)
		defer db.Close()
		appRequest := validAppOwnerRequest()
		appKey := appBundleKeyForTest(appRequest)
		tenantFacts, tenantRequest := validTenantClaimInput()
		required := requiredBundleKeySetForTest(appKey, tenantFacts.BundleKey)
		lease, err := NewLease(token, required)
		if err != nil {
			t.Fatal(err)
		}

		appFacts := appSourceFactsForTest(t, appRequest, required)
		appClaim, err := lease.BeginAppClaim(
			0,
			appFacts,
			appRequest,
		)
		if err != nil {
			t.Fatal(err)
		}
		completeAppClaimForLifecycleTest(t, appClaim)

		tenantFacts = tenantSourceFactsForTest(t, tenantFacts, required)
		tenantClaim, err := lease.BeginTenantClaim(1, tenantFacts, tenantRequest)
		if err != nil {
			t.Fatal(err)
		}
		completeTenantClaimForLifecycleTest(t, tenantClaim)
		if !appClaim.Active() || !tenantClaim.Active() {
			t.Fatal("completed claims did not retain one shared transaction")
		}

		mock.ExpectRollback()
		if err := lease.Close(); err != nil {
			t.Fatal(err)
		}
		if appClaim.Active() || tenantClaim.Active() {
			t.Fatal("owner claim remained active after shared lease close")
		}
		assertVerificationExpectations(t, mock)
	})

	t.Run("skipped App key rejects tenant claim", func(t *testing.T) {
		db, mock, token := newVerificationToken(t)
		defer db.Close()
		appRequest := validAppOwnerRequest()
		appKey := appBundleKeyForTest(appRequest)
		tenantFacts, tenantRequest := validTenantClaimInput()
		required := requiredBundleKeySetForTest(
			appKey,
			tenantFacts.BundleKey,
		)
		appFacts := appSourceFactsForTest(t, appRequest, required)
		tenantFacts = tenantSourceFactsForTest(t, tenantFacts, required)
		lease, err := NewLease(token, required)
		if err != nil {
			t.Fatal(err)
		}
		if _, err := lease.BeginTenantClaim(
			1,
			tenantFacts,
			tenantRequest,
		); !errors.Is(err, ErrClaimOrder) {
			t.Fatalf("skipped App key error = %v, want ErrClaimOrder", err)
		}
		appClaim, err := lease.BeginAppClaim(0, appFacts, appRequest)
		if err != nil {
			t.Fatal(err)
		}
		completeAppClaimForLifecycleTest(t, appClaim)
		tenantClaim, err := lease.BeginTenantClaim(1, tenantFacts, tenantRequest)
		if err != nil {
			t.Fatal(err)
		}
		completeTenantClaimForLifecycleTest(t, tenantClaim)

		mock.ExpectRollback()
		if err := lease.Close(); err != nil {
			t.Fatal(err)
		}
		assertVerificationExpectations(t, mock)
	})
}

func TestLeaseSealReceiptSetRejectsIncompleteCoverage(t *testing.T) {
	db, mock, token := newVerificationToken(t)
	defer db.Close()
	appRequest := validAppOwnerRequest()
	tenantFacts, _ := validTenantClaimInput()
	required := requiredBundleKeySetForTest(
		appBundleKeyForTest(appRequest),
		tenantFacts.BundleKey,
	)
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}
	appFacts := appSourceFactsForTest(t, appRequest, required)
	appClaim, err := lease.BeginAppClaim(0, appFacts, appRequest)
	if err != nil {
		t.Fatal(err)
	}
	completeAppClaimForLifecycleTest(t, appClaim)

	if _, _, err := lease.SealReceiptSet(); !errors.Is(err, ErrClaimOrder) {
		t.Fatalf("incomplete receipt set = %v, want ErrClaimOrder", err)
	}
	if !lease.Active() {
		t.Fatal("incomplete seal attempt closed the retained source lease")
	}

	mock.ExpectRollback()
	if err := lease.Close(); err != nil {
		t.Fatal(err)
	}
	assertVerificationExpectations(t, mock)
}

func TestLeaseSealReceiptSetReturnsCompleteCanonicalSetAndDigest(t *testing.T) {
	db, mock, token := newOwnedVerificationToken(t)
	defer db.Close()
	appRequest := validAppOwnerRequest()
	tenantFacts, tenantRequest := validTenantClaimInput()
	required := requiredBundleKeySetForTest(
		tenantFacts.BundleKey,
		appBundleKeyForTest(appRequest),
	)
	identity := sourceIdentityForRequiredSetTest(t, required)
	if err := bindOwnedSourceForTest(token, identity); err != nil {
		t.Fatal(err)
	}
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}

	appFacts := appSourceFactsForTest(t, appRequest, required)
	appFacts.SourceCatalogSnapshot = identity.catalogSnapshot
	appClaim, err := lease.BeginAppClaim(0, appFacts, appRequest)
	if err != nil {
		t.Fatal(err)
	}
	completeAppClaimForLifecycleTest(t, appClaim)

	tenantFacts = tenantSourceFactsForTest(t, tenantFacts, required)
	tenantFacts.SourceCatalogSnapshot = identity.catalogSnapshot
	tenantClaim, err := lease.BeginTenantClaim(1, tenantFacts, tenantRequest)
	if err != nil {
		t.Fatal(err)
	}
	completeTenantClaimForLifecycleTest(t, tenantClaim)

	set, digest, err := lease.SealReceiptSet()
	if err != nil {
		t.Fatal(err)
	}
	if err := layoutbundleownerverificationv1.ValidateReceiptSet(
		set,
		required,
	); err != nil {
		t.Fatal(err)
	}
	wantDigest, err := layoutbundleownerverificationv1.ReceiptSetDigest(
		set,
		required,
	)
	if err != nil {
		t.Fatal(err)
	}
	if digest != wantDigest || digest == "" {
		t.Fatalf("receipt set digest = %q, want %q", digest, wantDigest)
	}
	if len(set.Receipts) != 2 ||
		set.Receipts[0].BundleKey.SlotOwnerKind != layoutbundlekeyv1.SlotOwnerKindApp ||
		set.Receipts[1].BundleKey.SlotOwnerKind != layoutbundlekeyv1.SlotOwnerKindTenant {
		t.Fatalf("canonical receipt order = %#v", set.Receipts)
	}
	validated, validatedDigest, err := lease.ValidateSealedReceiptSet()
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(validated, set) || validatedDigest != digest {
		t.Fatalf("validated closure = %#v, %q; want %#v, %q", validated, validatedDigest, set, digest)
	}

	mock.ExpectRollback()
	expectSystemSessionRestore(mock)
	if err := lease.Close(); err != nil {
		t.Fatal(err)
	}
	assertVerificationExpectations(t, mock)
}

func TestLeaseSealReceiptSetAcceptsExactEmptyRequiredSet(t *testing.T) {
	db, mock, token := newOwnedVerificationToken(t)
	defer db.Close()
	required := requiredBundleKeySetForTest()
	identity := sourceIdentityForRequiredSetTest(t, required)
	if err := bindOwnedSourceForTest(token, identity); err != nil {
		t.Fatal(err)
	}
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}

	set, digest, err := lease.SealReceiptSet()
	if err != nil {
		t.Fatal(err)
	}
	if set.Receipts == nil || len(set.Receipts) != 0 {
		t.Fatalf("empty receipt set = %#v, want present empty slice", set.Receipts)
	}
	wantDigest, err := layoutbundleownerverificationv1.ReceiptSetDigest(
		set,
		required,
	)
	if err != nil {
		t.Fatal(err)
	}
	if digest != wantDigest || digest == "" {
		t.Fatalf("empty receipt set digest = %q, want %q", digest, wantDigest)
	}

	mock.ExpectRollback()
	expectSystemSessionRestore(mock)
	if err := lease.Close(); err != nil {
		t.Fatal(err)
	}
	assertVerificationExpectations(t, mock)
}

func TestReceiptMismatchAbortsTheCompleteRetainedLease(t *testing.T) {
	db, mock, token := newVerificationToken(t)
	defer db.Close()
	request := validAppOwnerRequest()
	required := requiredBundleKeySetForTest(appBundleKeyForTest(request))
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}
	facts := appSourceFactsForTest(t, request, required)
	claim, err := lease.BeginAppClaim(0, facts, request)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := claim.acquireInput(); err != nil {
		t.Fatal(err)
	}
	if err := lease.markClaimVerified(claim.lifecycle); err != nil {
		t.Fatal(err)
	}
	receipt := receiptForAppFacts(facts)
	receipt.SourceRequestDigest = appOwnerDigest("9")
	mock.ExpectRollback()
	if err := CompleteAppClaim(claim, receipt); !errors.Is(
		err,
		ErrAuthorityMismatch,
	) {
		t.Fatalf("foreign receipt = %v, want ErrAuthorityMismatch", err)
	}
	if lease.Active() || claim.Active() {
		t.Fatal("foreign receipt retained source or owner authority")
	}
	assertVerificationExpectations(t, mock)
}

func TestLeaseAllowsOnlyOnePendingClaimAcrossOwnerKinds(t *testing.T) {
	t.Run("App blocks tenant", func(t *testing.T) {
		db, mock, token := newVerificationToken(t)
		defer db.Close()
		appRequest := validAppOwnerRequest()
		appKey := appBundleKeyForTest(appRequest)
		tenantFacts, tenantRequest := validTenantClaimInput()
		required := requiredBundleKeySetForTest(appKey, tenantFacts.BundleKey)
		lease, err := NewLease(token, required)
		if err != nil {
			t.Fatal(err)
		}
		appClaim, err := lease.BeginAppClaim(
			0,
			appSourceFactsForTest(t, appRequest, required),
			appRequest,
		)
		if err != nil {
			t.Fatal(err)
		}
		tenantFacts = tenantSourceFactsForTest(t, tenantFacts, required)
		if _, err := lease.BeginTenantClaim(
			1,
			tenantFacts,
			tenantRequest,
		); !errors.Is(err, ErrClosed) {
			t.Fatalf("concurrent tenant claim error = %v, want ErrClosed", err)
		}
		mock.ExpectRollback()
		appClaim.Abort()
		assertVerificationExpectations(t, mock)
	})

	t.Run("tenant blocks App", func(t *testing.T) {
		db, mock, token := newVerificationToken(t)
		defer db.Close()
		tenantFacts, tenantRequest := validTenantClaimInput()
		appRequest := validAppOwnerRequest()
		required := requiredBundleKeySetForTest(tenantFacts.BundleKey)
		lease, err := NewLease(token, required)
		if err != nil {
			t.Fatal(err)
		}
		tenantFacts = tenantSourceFactsForTest(t, tenantFacts, required)
		tenantClaim, err := lease.BeginTenantClaim(0, tenantFacts, tenantRequest)
		if err != nil {
			t.Fatal(err)
		}
		if _, err := lease.BeginAppClaim(
			1,
			appSourceFactsForTest(t, appRequest, required),
			appRequest,
		); !errors.Is(err, ErrClosed) {
			t.Fatalf("concurrent App claim error = %v, want ErrClosed", err)
		}
		mock.ExpectRollback()
		tenantClaim.Abort()
		assertVerificationExpectations(t, mock)
	})

	t.Run("system blocks App and tenant", func(t *testing.T) {
		db, mock, token := newVerificationToken(t)
		defer db.Close()
		systemFacts, systemRequest := validSystemClaimInput()
		required := requiredBundleKeySetForTest(systemFacts.BundleKey)
		lease, err := NewLease(token, required)
		if err != nil {
			t.Fatal(err)
		}
		systemFacts = systemSourceFactsForTest(t, systemFacts, required)
		systemClaim, err := lease.BeginSystemClaim(
			0,
			systemFacts,
			systemRequest,
		)
		if err != nil {
			t.Fatal(err)
		}
		appRequest := validAppOwnerRequest()
		if _, err := lease.BeginAppClaim(
			1,
			appSourceFactsForTest(t, appRequest, required),
			appRequest,
		); !errors.Is(err, ErrClosed) {
			t.Fatalf("concurrent App claim error = %v, want ErrClosed", err)
		}
		tenantFacts, tenantRequest := validTenantClaimInput()
		if _, err := lease.BeginTenantClaim(
			1,
			tenantSourceFactsForTest(t, tenantFacts, required),
			tenantRequest,
		); !errors.Is(err, ErrClosed) {
			t.Fatalf("concurrent tenant claim error = %v, want ErrClosed", err)
		}
		mock.ExpectRollback()
		systemClaim.Abort()
		assertVerificationExpectations(t, mock)
	})
}

func TestTenantClaimRetainsExactFactsAndRejectsUseAfterClose(t *testing.T) {
	db, mock, token := newVerificationToken(t)
	defer db.Close()
	facts, request := validTenantClaimInput()
	required := requiredBundleKeySetForTest(facts.BundleKey)
	facts = tenantSourceFactsForTest(t, facts, required)
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}
	claim, err := lease.BeginTenantClaim(0, facts, request)
	if err != nil {
		t.Fatal(err)
	}
	gotFacts, err := claim.Facts()
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(gotFacts, facts) {
		t.Fatalf("tenant source facts = %#v, want %#v", gotFacts, facts)
	}
	gotToken, gotRequest, err := claim.acquireInput()
	if err != nil {
		t.Fatal(err)
	}
	if gotToken != token || !reflect.DeepEqual(gotRequest, request) {
		t.Fatalf("tenant owner input = %p, %#v", gotToken, gotRequest)
	}
	if err := lease.markClaimVerified(claim.lifecycle); err != nil {
		t.Fatal(err)
	}
	if _, err := json.Marshal(claim); !errors.Is(err, ErrCapabilityHasNoJSON) {
		t.Fatalf("tenant claim JSON marshal = %v", err)
	}
	if err := json.Unmarshal([]byte(`{}`), claim); !errors.Is(
		err,
		ErrCapabilityHasNoJSON,
	) {
		t.Fatalf("tenant claim JSON unmarshal = %v", err)
	}
	if err := CompleteTenantClaim(claim, receiptForTenantFacts(facts)); err != nil {
		t.Fatal(err)
	}
	if !claim.Active() {
		t.Fatal("completed tenant claim is not active")
	}

	mock.ExpectRollback()
	if err := lease.Close(); err != nil {
		t.Fatal(err)
	}
	if claim.Active() {
		t.Fatal("tenant claim remained active after close")
	}
	if _, err := claim.Facts(); !errors.Is(err, ErrClosed) {
		t.Fatalf("tenant facts after close = %v, want ErrClosed", err)
	}
	if _, _, err := claim.acquireInput(); !errors.Is(err, ErrClosed) {
		t.Fatalf("tenant owner input after close = %v, want ErrClosed", err)
	}
	if err := CompleteTenantClaim(
		claim,
		receiptForTenantFacts(facts),
	); !errors.Is(err, ErrClosed) {
		t.Fatalf("tenant claim completion after close = %v, want ErrClosed", err)
	}
	assertVerificationExpectations(t, mock)
}

func TestSystemClaimRequiresAuthorityFenceAndRejectsUseAfterClose(t *testing.T) {
	db, mock, token := newVerificationToken(t)
	defer db.Close()
	facts, request := validSystemClaimInput()
	required := requiredBundleKeySetForTest(facts.BundleKey)
	facts = systemSourceFactsForTest(t, facts, required)
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}
	claim, err := lease.BeginSystemClaim(0, facts, request)
	if err != nil {
		t.Fatal(err)
	}
	gotFacts, err := claim.Facts()
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(gotFacts, facts) {
		t.Fatalf("system source facts = %#v, want %#v", gotFacts, facts)
	}
	gotToken, gotRequest, err := claim.acquireInput()
	if err != nil {
		t.Fatal(err)
	}
	if gotToken != token || !reflect.DeepEqual(gotRequest, request) {
		t.Fatalf("system owner input = %p, %#v", gotToken, gotRequest)
	}
	if _, err := json.Marshal(claim); !errors.Is(err, ErrCapabilityHasNoJSON) {
		t.Fatalf("system claim JSON marshal = %v", err)
	}
	if err := json.Unmarshal([]byte(`{}`), claim); !errors.Is(
		err,
		ErrCapabilityHasNoJSON,
	) {
		t.Fatalf("system claim JSON unmarshal = %v", err)
	}
	if err := lease.markClaimVerified(claim.lifecycle); err != nil {
		t.Fatal(err)
	}
	if err := CompleteSystemClaim(
		claim,
		receiptForSystemFacts(facts),
	); !errors.Is(err, ErrClosed) {
		t.Fatalf("system claim completed without authority fence: %v", err)
	}
	if claim.Active() {
		t.Fatal("system claim became active without authority fence")
	}

	mock.ExpectRollback()
	if err := lease.Close(); err != nil {
		t.Fatal(err)
	}
	if claim.Active() {
		t.Fatal("system claim remained active after close")
	}
	if _, err := claim.Facts(); !errors.Is(err, ErrClosed) {
		t.Fatalf("system facts after close = %v, want ErrClosed", err)
	}
	if _, _, err := claim.acquireInput(); !errors.Is(err, ErrClosed) {
		t.Fatalf("system owner input after close = %v, want ErrClosed", err)
	}
	if err := CompleteSystemClaim(
		claim,
		receiptForSystemFacts(facts),
	); !errors.Is(err, ErrClosed) {
		t.Fatalf("system claim completion after close = %v, want ErrClosed", err)
	}
	assertVerificationExpectations(t, mock)
}

func TestOwnerClaimsRequireVerifiedStateBeforeCompletion(t *testing.T) {
	for _, kind := range []string{"App", "tenant", "system"} {
		t.Run(kind, func(t *testing.T) {
			db, mock, token := newVerificationToken(t)
			defer db.Close()
			lease, err := NewLease(token, requiredBundleKeySetForKind(kind))
			if err != nil {
				t.Fatal(err)
			}
			claim := newOwnerClaimLifecycleHarness(t, lease, kind)

			if err := claim.complete(); !errors.Is(err, ErrClosed) {
				t.Fatalf("completion while pending = %v, want ErrClosed", err)
			}
			if !lease.Active() || claim.active() {
				t.Fatal("pending completion attempt changed claim lifetime")
			}
			if err := claim.acquire(); err != nil {
				t.Fatal(err)
			}
			if err := claim.complete(); !errors.Is(err, ErrClosed) {
				t.Fatalf("completion while verifying = %v, want ErrClosed", err)
			}
			if err := lease.markClaimVerified(claim.lifecycle); err != nil {
				t.Fatal(err)
			}
			if kind == "system" {
				if err := claim.complete(); !errors.Is(err, ErrClosed) {
					t.Fatalf(
						"system completion without authority fence = %v, want ErrClosed",
						err,
					)
				}
			} else {
				if err := claim.complete(); err != nil {
					t.Fatal(err)
				}
				if !claim.active() {
					t.Fatal("completed verified claim is not active")
				}
				if err := claim.complete(); !errors.Is(err, ErrClosed) {
					t.Fatalf("second completion = %v, want ErrClosed", err)
				}
			}

			mock.ExpectRollback()
			if err := lease.Close(); err != nil {
				t.Fatal(err)
			}
			assertVerificationExpectations(t, mock)
		})
	}
}

func TestOwnerClaimVerificationInputIsOneShot(t *testing.T) {
	for _, kind := range []string{"App", "tenant", "system"} {
		t.Run(kind, func(t *testing.T) {
			db, mock, token := newVerificationToken(t)
			defer db.Close()
			lease, err := NewLease(token, requiredBundleKeySetForKind(kind))
			if err != nil {
				t.Fatal(err)
			}
			claim := newOwnerClaimLifecycleHarness(t, lease, kind)

			if err := claim.acquire(); err != nil {
				t.Fatal(err)
			}
			if err := claim.acquire(); !errors.Is(err, ErrClosed) {
				t.Fatalf("second sequential acquire = %v, want ErrClosed", err)
			}
			if err := lease.markClaimVerified(claim.lifecycle); err != nil {
				t.Fatalf("failed replay changed verifying state: %v", err)
			}

			mock.ExpectRollback()
			if err := lease.Close(); err != nil {
				t.Fatal(err)
			}
			assertVerificationExpectations(t, mock)
		})
	}
}

func TestOwnerClaimVerificationInputHasOneConcurrentWinner(t *testing.T) {
	const contenders = 16
	for _, kind := range []string{"App", "tenant", "system"} {
		t.Run(kind, func(t *testing.T) {
			db, mock, token := newVerificationToken(t)
			defer db.Close()
			lease, err := NewLease(token, requiredBundleKeySetForKind(kind))
			if err != nil {
				t.Fatal(err)
			}
			claim := newOwnerClaimLifecycleHarness(t, lease, kind)
			start := make(chan struct{})
			results := make(chan error, contenders)
			for index := 0; index < contenders; index++ {
				go func() {
					<-start
					results <- claim.acquire()
				}()
			}
			close(start)

			successes := 0
			for index := 0; index < contenders; index++ {
				err := <-results
				if err == nil {
					successes++
					continue
				}
				if !errors.Is(err, ErrClosed) {
					t.Fatalf("concurrent acquire = %v, want ErrClosed", err)
				}
			}
			if successes != 1 {
				t.Fatalf("concurrent acquire successes = %d, want 1", successes)
			}
			if err := lease.markClaimVerified(claim.lifecycle); err != nil {
				t.Fatal(err)
			}

			mock.ExpectRollback()
			if err := lease.Close(); err != nil {
				t.Fatal(err)
			}
			assertVerificationExpectations(t, mock)
		})
	}
}

func TestOwnerClaimCloseAbortsInFlightVerification(t *testing.T) {
	for _, kind := range []string{"App", "tenant", "system"} {
		t.Run(kind, func(t *testing.T) {
			db, mock, token := newVerificationToken(t)
			defer db.Close()
			lease, err := NewLease(token, requiredBundleKeySetForKind(kind))
			if err != nil {
				t.Fatal(err)
			}
			claim := newOwnerClaimLifecycleHarness(t, lease, kind)
			if err := claim.acquire(); err != nil {
				t.Fatal(err)
			}

			mock.ExpectRollback()
			if err := lease.Close(); err != nil {
				t.Fatal(err)
			}
			if err := lease.markClaimVerified(claim.lifecycle); !errors.Is(
				err,
				ErrClosed,
			) {
				t.Fatalf("verification after close = %v, want ErrClosed", err)
			}
			if err := claim.complete(); !errors.Is(err, ErrClosed) {
				t.Fatalf("completion after close = %v, want ErrClosed", err)
			}
			if err := claim.acquire(); !errors.Is(err, ErrClosed) {
				t.Fatalf("acquire after close = %v, want ErrClosed", err)
			}
			if err := claim.facts(); !errors.Is(err, ErrClosed) {
				t.Fatalf("facts after close = %v, want ErrClosed", err)
			}
			assertVerificationExpectations(t, mock)
		})
	}
}

func TestVerifyOwnerClaimFailureAbortsRetainedLease(t *testing.T) {
	for _, kind := range []string{"App", "tenant", "system"} {
		t.Run(kind, func(t *testing.T) {
			db, mock, token := newVerificationToken(t)
			defer db.Close()
			lease, err := NewLease(token, requiredBundleKeySetForKind(kind))
			if err != nil {
				t.Fatal(err)
			}
			claim := newOwnerClaimLifecycleHarness(t, lease, kind)

			mock.ExpectRollback()
			if err := claim.verifyWithWrongAuthority(); !errors.Is(
				err,
				ErrAuthorityMismatch,
			) {
				t.Fatalf(
					"verification with wrong authority = %v, want ErrAuthorityMismatch",
					err,
				)
			}
			if lease.Active() || claim.active() {
				t.Fatal("failed verification retained the source lease")
			}
			if err := claim.verifyWithWrongAuthority(); !errors.Is(err, ErrClosed) {
				t.Fatalf("verification replay after failure = %v, want ErrClosed", err)
			}
			if err := claim.complete(); !errors.Is(err, ErrClosed) {
				t.Fatalf("completion after failure = %v, want ErrClosed", err)
			}
			if err := claim.facts(); !errors.Is(err, ErrClosed) {
				t.Fatalf("facts after failure = %v, want ErrClosed", err)
			}
			assertVerificationExpectations(t, mock)
		})
	}
}

type ownerClaimLifecycleHarness struct {
	lifecycle                *claimLifecycle
	acquire                  func() error
	complete                 func() error
	active                   func() bool
	facts                    func() error
	verifyWithWrongAuthority func() error
}

func newOwnerClaimLifecycleHarness(
	t *testing.T,
	lease *Lease,
	kind string,
) ownerClaimLifecycleHarness {
	t.Helper()
	switch kind {
	case "App":
		request := validAppOwnerRequest()
		required := requiredBundleKeySetForKind(kind)
		facts := appSourceFactsForTest(t, request, required)
		claim, err := lease.BeginAppClaim(
			0,
			facts,
			request,
		)
		if err != nil {
			t.Fatal(err)
		}
		return ownerClaimLifecycleHarness{
			lifecycle: claim.lifecycle,
			acquire: func() error {
				_, _, err := claim.acquireInput()
				return err
			},
			complete: func() error {
				return CompleteAppClaim(claim, receiptForAppFacts(facts))
			},
			active: claim.Active,
			facts: func() error {
				_, err := claim.Facts()
				return err
			},
			verifyWithWrongAuthority: func() error {
				_, err := VerifyAppOwnerClaim(context.Background(), nil, claim)
				return err
			},
		}
	case "tenant":
		facts, request := validTenantClaimInput()
		facts = tenantSourceFactsForTest(
			t,
			facts,
			requiredBundleKeySetForKind(kind),
		)
		claim, err := lease.BeginTenantClaim(0, facts, request)
		if err != nil {
			t.Fatal(err)
		}
		return ownerClaimLifecycleHarness{
			lifecycle: claim.lifecycle,
			acquire: func() error {
				_, _, err := claim.acquireInput()
				return err
			},
			complete: func() error {
				return CompleteTenantClaim(claim, receiptForTenantFacts(facts))
			},
			active: claim.Active,
			facts: func() error {
				_, err := claim.Facts()
				return err
			},
			verifyWithWrongAuthority: func() error {
				_, err := VerifyTenantOwnerClaim(context.Background(), nil, claim)
				return err
			},
		}
	case "system":
		facts, request := validSystemClaimInput()
		facts = systemSourceFactsForTest(
			t,
			facts,
			requiredBundleKeySetForKind(kind),
		)
		claim, err := lease.BeginSystemClaim(0, facts, request)
		if err != nil {
			t.Fatal(err)
		}
		return ownerClaimLifecycleHarness{
			lifecycle: claim.lifecycle,
			acquire: func() error {
				_, _, err := claim.acquireInput()
				return err
			},
			complete: func() error {
				return CompleteSystemClaim(claim, receiptForSystemFacts(facts))
			},
			active: claim.Active,
			facts: func() error {
				_, err := claim.Facts()
				return err
			},
			verifyWithWrongAuthority: func() error {
				_, err := VerifySystemOwnerClaim(context.Background(), nil, claim)
				return err
			},
		}
	default:
		t.Fatalf("unknown owner claim kind %q", kind)
		return ownerClaimLifecycleHarness{}
	}
}

func validTenantClaimInput() (TenantSourceFacts, TenantOwnerRequest) {
	key := layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindTenant,
		SlotOwnerNamespace: "io.example.tenant",
		LocalComponentKey: layoutbundlekeyv1.LocalComponentKey(
			"key256:" + strings.Repeat("2", 64),
		),
	}
	facts := TenantSourceFacts{
		OrgID:     testAppOrgID,
		BundleKey: key,
	}
	return facts, TenantOwnerRequest{OrgID: facts.OrgID, BundleKey: key}
}

func validSystemClaimInput() (SystemSourceFacts, SystemOwnerRequest) {
	key := layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindSystem,
		SlotOwnerNamespace: layoutbundlekeyv1.SystemSlotOwnerNamespace,
		LocalComponentKey: layoutbundlekeyv1.LocalComponentKey(
			"key256:" + strings.Repeat("3", 64),
		),
	}
	facts := SystemSourceFacts{
		OrgID:     testAppOrgID,
		BundleKey: key,
	}
	return facts, SystemOwnerRequest{BundleKey: key}
}

func completeAppClaimForLifecycleTest(t *testing.T, claim *AppClaim) {
	t.Helper()
	if _, _, err := claim.acquireInput(); err != nil {
		t.Fatal(err)
	}
	if err := claim.lease.markClaimVerified(claim.lifecycle); err != nil {
		t.Fatal(err)
	}
	if err := CompleteAppClaim(claim, receiptForAppFacts(claim.facts)); err != nil {
		t.Fatal(err)
	}
}

func completeTenantClaimForLifecycleTest(t *testing.T, claim *TenantClaim) {
	t.Helper()
	if _, _, err := claim.acquireInput(); err != nil {
		t.Fatal(err)
	}
	if err := claim.lease.markClaimVerified(claim.lifecycle); err != nil {
		t.Fatal(err)
	}
	if err := CompleteTenantClaim(
		claim,
		receiptForTenantFacts(claim.facts),
	); err != nil {
		t.Fatal(err)
	}
}

const testClaimWriterCommandID = "018f0000-0000-7000-9000-000000000099"

func appBundleKeyForTest(request AppOwnerRequest) layoutbundlekeyv1.BundleKey {
	return layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindApp,
		SlotOwnerNamespace: request.AppID,
		LocalComponentKey:  request.LocalComponentKey,
	}
}

func requiredBundleKeySetForTest(
	keys ...layoutbundlekeyv1.BundleKey,
) layoutbundlekeyv1.RequiredBundleKeySet {
	return layoutbundlekeyv1.RequiredBundleKeySet{
		Version:    layoutbundlekeyv1.RequiredBundleKeySetVersion,
		BundleKeys: append([]layoutbundlekeyv1.BundleKey{}, keys...),
	}
}

func requiredBundleKeySetForKind(kind string) layoutbundlekeyv1.RequiredBundleKeySet {
	switch kind {
	case "App":
		return requiredBundleKeySetForTest(
			appBundleKeyForTest(validAppOwnerRequest()),
		)
	case "tenant":
		facts, _ := validTenantClaimInput()
		return requiredBundleKeySetForTest(facts.BundleKey)
	case "system":
		facts, _ := validSystemClaimInput()
		return requiredBundleKeySetForTest(facts.BundleKey)
	default:
		return layoutbundlekeyv1.RequiredBundleKeySet{}
	}
}

func requiredBundleKeySetDigestForTest(
	t *testing.T,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) string {
	t.Helper()
	digest, err := layoutbundlekeyv1.RequiredBundleKeySetDigest(required)
	if err != nil {
		t.Fatal(err)
	}
	return digest
}

func appSourceFactsForTest(
	t *testing.T,
	request AppOwnerRequest,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) AppSourceFacts {
	t.Helper()
	return AppSourceFacts{
		OrgID:                       request.OrgID,
		WriterCommandID:             testClaimWriterCommandID,
		WriterReservationGeneration: 7,
		SourceRequestDigest:         appOwnerDigest("a"),
		RequiredBundleKeySetDigest:  requiredBundleKeySetDigestForTest(t, required),
		BundleKey:                   appBundleKeyForTest(request),
		AppID:                       request.AppID,
		AppInstallationID:           request.AppInstallationID,
		AppReleaseDigest:            request.AppReleaseDigest,
	}
}

func tenantSourceFactsForTest(
	t *testing.T,
	facts TenantSourceFacts,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) TenantSourceFacts {
	t.Helper()
	facts.WriterCommandID = testClaimWriterCommandID
	facts.WriterReservationGeneration = 7
	facts.SourceRequestDigest = appOwnerDigest("a")
	facts.RequiredBundleKeySetDigest = requiredBundleKeySetDigestForTest(t, required)
	return facts
}

func systemSourceFactsForTest(
	t *testing.T,
	facts SystemSourceFacts,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) SystemSourceFacts {
	t.Helper()
	facts.WriterCommandID = testClaimWriterCommandID
	facts.WriterReservationGeneration = 7
	facts.SourceRequestDigest = appOwnerDigest("a")
	facts.RequiredBundleKeySetDigest = requiredBundleKeySetDigestForTest(t, required)
	return facts
}

func receiptForAppFacts(
	facts AppSourceFacts,
) layoutbundleownerverificationv1.Receipt {
	return receiptForClaimFacts(
		facts.OrgID,
		facts.WriterCommandID,
		facts.WriterReservationGeneration,
		facts.SourceRequestDigest,
		facts.RequiredBundleKeySetDigest,
		facts.BundleKey,
	)
}

func receiptForTenantFacts(
	facts TenantSourceFacts,
) layoutbundleownerverificationv1.Receipt {
	return receiptForClaimFacts(
		facts.OrgID,
		facts.WriterCommandID,
		facts.WriterReservationGeneration,
		facts.SourceRequestDigest,
		facts.RequiredBundleKeySetDigest,
		facts.BundleKey,
	)
}

func receiptForSystemFacts(
	facts SystemSourceFacts,
) layoutbundleownerverificationv1.Receipt {
	return receiptForClaimFacts(
		facts.OrgID,
		facts.WriterCommandID,
		facts.WriterReservationGeneration,
		facts.SourceRequestDigest,
		facts.RequiredBundleKeySetDigest,
		facts.BundleKey,
	)
}

func receiptForClaimFacts(
	orgID string,
	writerCommandID string,
	writerReservationGeneration uint64,
	sourceRequestDigest string,
	requiredBundleKeySetDigest string,
	key layoutbundlekeyv1.BundleKey,
) layoutbundleownerverificationv1.Receipt {
	evidenceVersion := ""
	evidenceDigit := "0"
	switch key.SlotOwnerKind {
	case layoutbundlekeyv1.SlotOwnerKindApp:
		evidenceVersion = layoutbundleownerverificationv1.AppAuthorityEvidenceVersion
		evidenceDigit = "1"
	case layoutbundlekeyv1.SlotOwnerKindTenant:
		evidenceVersion = layoutbundleownerverificationv1.TenantAuthorityEvidenceVersion
		evidenceDigit = "2"
	case layoutbundlekeyv1.SlotOwnerKindSystem:
		evidenceVersion = layoutbundleownerverificationv1.SystemAuthorityEvidenceVersion
		evidenceDigit = "3"
	}
	return layoutbundleownerverificationv1.Receipt{
		ReceiptVersion:              layoutbundleownerverificationv1.ReceiptVersion,
		OrgID:                       orgID,
		WriterCommandID:             writerCommandID,
		WriterReservationGeneration: writerReservationGeneration,
		SourceRequestDigest:         sourceRequestDigest,
		RequiredBundleKeySetDigest:  requiredBundleKeySetDigest,
		BundleKey:                   key,
		EvidenceVersion:             evidenceVersion,
		EvidenceDigest:              appOwnerDigest(evidenceDigit),
		VerificationState:           layoutbundleownerverificationv1.VerificationStateVerified,
	}
}
