package catalogverificationtx

import (
	"errors"
	"testing"

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

func TestOwnedLeaseRequiresOneShotExactSourceBinding(t *testing.T) {
	db, mock, token := newOwnedVerificationToken(t)
	defer db.Close()
	required := requiredBundleKeySetForTest(
		appBundleKeyForTest(validAppOwnerRequest()),
	)
	identity := sourceIdentityForRequiredSetTest(t, required)

	if _, err := NewLease(token, required); !errors.Is(err, ErrAuthorityMismatch) {
		t.Fatalf("unbound owned lease = %v, want ErrAuthorityMismatch", err)
	}
	if err := bindOwnedSourceForTest(token, identity); err != nil {
		t.Fatal(err)
	}
	if err := bindOwnedSourceForTest(token, identity); !errors.Is(
		err,
		ErrAuthorityMismatch,
	) {
		t.Fatalf("second source binding = %v, want ErrAuthorityMismatch", err)
	}
	lease, err := NewLease(token, required)
	if err != nil {
		t.Fatal(err)
	}
	if !lease.Active() {
		t.Fatal("exact source binding did not keep the owned lease active")
	}

	mock.ExpectRollback()
	expectSystemSessionRestore(mock)
	if err := lease.Close(); err != nil {
		t.Fatal(err)
	}
	token.mu.RLock()
	detached := token.source
	token.mu.RUnlock()
	if detached != (sourceBinding{}) {
		t.Fatalf("detached source binding = %#v, want zero", detached)
	}
	if lease.Active() {
		t.Fatal("closed lease retained its source binding")
	}
	assertVerificationExpectations(t, mock)
}

func TestOwnedLeaseRejectsSourceIdentitySubstitution(t *testing.T) {
	db, mock, token := newOwnedVerificationToken(t)
	defer db.Close()
	request := validAppOwnerRequest()
	required := requiredBundleKeySetForTest(appBundleKeyForTest(request))
	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)
	}
	facts := AppSourceFacts{
		SourceCatalogSnapshot:       identity.catalogSnapshot,
		OrgID:                       identity.orgID,
		WriterCommandID:             identity.writerCommandID,
		WriterReservationGeneration: identity.writerReservationGeneration,
		SourceRequestDigest:         identity.sourceRequestDigest,
		RequiredBundleKeySetDigest:  identity.requiredBundleKeySetDigest,
		BundleKey: layoutbundlekeyv1.BundleKey{
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindApp,
			SlotOwnerNamespace: request.AppID,
			LocalComponentKey:  request.LocalComponentKey,
		},
		AppID:             request.AppID,
		AppInstallationID: request.AppInstallationID,
		AppReleaseDigest:  request.AppReleaseDigest,
	}
	tests := map[string]func(*AppSourceFacts){
		"SourceCatalogSnapshot": func(value *AppSourceFacts) {
			value.SourceCatalogSnapshot.CatalogRevision++
		},
		"OrgID": func(value *AppSourceFacts) {
			value.OrgID = "org-foreign"
		},
		"WriterCommandID": func(value *AppSourceFacts) {
			value.WriterCommandID = "writer-foreign"
		},
		"WriterReservationGeneration": func(value *AppSourceFacts) {
			value.WriterReservationGeneration++
		},
		"SourceRequestDigest": func(value *AppSourceFacts) {
			value.SourceRequestDigest = "sha256:foreign-source"
		},
		"RequiredBundleKeySetDigest": func(value *AppSourceFacts) {
			value.RequiredBundleKeySetDigest = "sha256:foreign-required-set"
		},
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			substituted := facts
			mutate(&substituted)
			if _, err := lease.BeginAppClaim(
				0,
				substituted,
				request,
			); !errors.Is(err, ErrAuthorityMismatch) {
				t.Fatalf(
					"substituted source identity = %v, want ErrAuthorityMismatch",
					err,
				)
			}
			if !lease.Active() {
				t.Fatal("rejected identity substitution closed the source lease")
			}
		})
	}
	if _, err := lease.BeginAppClaim(0, facts, request); err != nil {
		t.Fatalf("exact source identity = %v", err)
	}

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

func TestOwnedLeaseMatchesTenantAndSystemClaimsToSourceBinding(t *testing.T) {
	tests := map[string]func(*Lease, sourceIdentity) error{
		"tenant": func(lease *Lease, identity sourceIdentity) error {
			facts, request := validTenantClaimInput()
			facts.SourceCatalogSnapshot = identity.catalogSnapshot
			facts.OrgID = identity.orgID
			facts.WriterCommandID = identity.writerCommandID
			facts.WriterReservationGeneration = identity.writerReservationGeneration
			facts.SourceRequestDigest = identity.sourceRequestDigest
			facts.RequiredBundleKeySetDigest = identity.requiredBundleKeySetDigest
			if _, err := lease.BeginTenantClaim(0, facts, request); err != nil {
				return err
			}
			return nil
		},
		"system": func(lease *Lease, identity sourceIdentity) error {
			facts, request := validSystemClaimInput()
			facts.SourceCatalogSnapshot = identity.catalogSnapshot
			facts.OrgID = identity.orgID
			facts.WriterCommandID = identity.writerCommandID
			facts.WriterReservationGeneration = identity.writerReservationGeneration
			facts.SourceRequestDigest = identity.sourceRequestDigest
			facts.RequiredBundleKeySetDigest = identity.requiredBundleKeySetDigest
			if _, err := lease.BeginSystemClaim(0, facts, request); err != nil {
				return err
			}
			return nil
		},
	}
	for name, begin := range tests {
		t.Run(name, func(t *testing.T) {
			db, mock, token := newOwnedVerificationToken(t)
			defer db.Close()
			var required layoutbundlekeyv1.RequiredBundleKeySet
			switch name {
			case "tenant":
				facts, _ := validTenantClaimInput()
				required = requiredBundleKeySetForTest(facts.BundleKey)
			case "system":
				facts, _ := validSystemClaimInput()
				required = requiredBundleKeySetForTest(facts.BundleKey)
			}
			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)
			}
			if err := begin(lease, identity); err != nil {
				t.Fatal(err)
			}

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

func TestValidatedSourceBindingRejectsLegacyAndInvalidTokens(t *testing.T) {
	identity := sourceIdentityForRequiredSetTest(
		t,
		requiredBundleKeySetForTest(),
	)
	var nilToken *Token
	if err := bindOwnedSourceForTest(nilToken, identity); !errors.Is(
		err,
		ErrClosed,
	) {
		t.Fatalf("nil token source binding = %v, want ErrClosed", err)
	}

	legacyDB, legacyMock, legacyToken := newVerificationToken(t)
	defer legacyDB.Close()
	if err := bindOwnedSourceForTest(legacyToken, identity); !errors.Is(
		err,
		ErrAuthorityMismatch,
	) {
		t.Fatalf("legacy token source binding = %v, want ErrAuthorityMismatch", err)
	}
	legacyMock.ExpectRollback()
	if err := legacyToken.Close(); err != nil {
		t.Fatal(err)
	}
	assertVerificationExpectations(t, legacyMock)

	ownedDB, ownedMock, ownedToken := newOwnedVerificationToken(t)
	defer ownedDB.Close()
	invalidIdentity := identity
	invalidIdentity.catalogSnapshot.EngineGeneration = 0
	if err := bindOwnedSourceForTest(
		ownedToken,
		invalidIdentity,
	); !errors.Is(err, ErrAuthorityMismatch) {
		t.Fatalf("invalid source snapshot binding = %v, want ErrAuthorityMismatch", err)
	}
	ownedMock.ExpectRollback()
	expectSystemSessionRestore(ownedMock)
	if err := ownedToken.Close(); err != nil {
		t.Fatal(err)
	}
	if err := bindOwnedSourceForTest(ownedToken, identity); !errors.Is(
		err,
		ErrClosed,
	) {
		t.Fatalf("closed token source binding = %v, want ErrClosed", err)
	}
	assertVerificationExpectations(t, ownedMock)
}

func sourceIdentityForRequiredSetTest(
	t *testing.T,
	required layoutbundlekeyv1.RequiredBundleKeySet,
) sourceIdentity {
	t.Helper()
	return sourceIdentity{
		catalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
			OrgID:                 testAppOrgID,
			CatalogRevision:       11,
			CatalogSnapshotDigest: appOwnerDigest("8"),
			SchemaGeneration:      12,
			EngineGeneration:      13,
		},
		orgID:                       testAppOrgID,
		writerCommandID:             "018f0000-0000-7000-9000-000000000099",
		writerReservationGeneration: 7,
		sourceRequestDigest:         appOwnerDigest("a"),
		requiredBundleKeySetDigest:  requiredBundleKeySetDigestForTest(t, required),
	}
}

func bindOwnedSourceForTest(token *Token, identity sourceIdentity) error {
	return token.BindValidatedSource(
		identity.catalogSnapshot,
		identity.orgID,
		identity.writerCommandID,
		identity.writerReservationGeneration,
		identity.sourceRequestDigest,
		identity.requiredBundleKeySetDigest,
	)
}
