package connector_oauth

import (
	"context"
	"database/sql"
	"errors"
	"testing"
	"time"
)

// Preserve the released positional shape while scoped token resolution evolves
// through a distinct request type.
var _ = ResolveAccessTokenRequest{"user-1", "provider-a", 1}

type fakeCleanupAudit struct {
	events []CleanupAuditEvent
	err    error
}

type disconnectHookRepository struct {
	oauthRepository
	beforeDisconnect func()
}

func (r disconnectHookRepository) disconnect(ctx context.Context, userID, connectorKey string, expectedGeneration int64, state string, now time.Time) (disconnectResult, error) {
	if r.beforeDisconnect != nil {
		r.beforeDisconnect()
	}
	return r.oauthRepository.disconnect(ctx, userID, connectorKey, expectedGeneration, state, now)
}

type purposeFailCipher struct {
	SecretCipher
	sealPurpose SecretPurpose
	openPurpose SecretPurpose
	err         error
}

func (c purposeFailCipher) Seal(plaintext []byte, secretContext SecretContext) (string, string, error) {
	if secretContext.Purpose == c.sealPurpose {
		return "", "", c.err
	}
	return c.SecretCipher.Seal(plaintext, secretContext)
}

func (c purposeFailCipher) Open(ciphertext string, secretContext SecretContext) ([]byte, error) {
	if secretContext.Purpose == c.openPurpose {
		return nil, c.err
	}
	return c.SecretCipher.Open(ciphertext, secretContext)
}

func (a *fakeCleanupAudit) EmitOAuthCleanup(_ context.Context, _ *sql.Tx, event CleanupAuditEvent) error {
	if a.err != nil {
		return a.err
	}
	a.events = append(a.events, event)
	return nil
}

func testProvider() *fakeProvider {
	expires := time.Now().Add(time.Hour)
	return &fakeProvider{
		terminationBound: time.Second,
		authorizationURL: "https://authorize.invalid/start",
		identity: RemoteIdentity{Issuer: "issuer.invalid", Subject: "subject-1",
			PrincipalAddress: "person@example.invalid", DisplayName: "Example"},
		tokens: TokenSet{AccessToken: "access-secret", RefreshToken: "refresh-secret",
			TokenType: "Bearer", Scopes: []string{"scope.read"}, ExpiresAt: &expires},
		refreshed: TokenSet{AccessToken: "access-refreshed", RefreshToken: "refresh-rotated",
			TokenType: "Bearer", Scopes: []string{"scope.read"}, ExpiresAt: &expires},
	}
}

func beginRequest() BeginAuthorizationRequest {
	return BeginAuthorizationRequest{UserID: "user-1", ConnectorKey: "provider-a",
		RedirectURI: "https://callback.invalid/oauth", Scopes: []string{"scope.read"}}
}

func connectPrincipal(t *testing.T, repository *memoryRepository, provider *fakeProvider) *ConnectedPrincipal {
	t.Helper()
	service := newTestService(repository, provider)
	start, err := service.BeginAuthorization(context.Background(), beginRequest())
	if err != nil || start.Outcome != AuthorizationOutcomeRedirect {
		t.Fatalf("BeginAuthorization = %+v, %v", start, err)
	}
	state := provider.authorizations[len(provider.authorizations)-1].State
	completion, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code-secret",
	})
	if err != nil || completion.Outcome != AuthorizationCompletionResultStaged {
		t.Fatalf("CompleteAuthorization = %+v, %v", completion, err)
	}
	restarted := newTestService(repository, provider)
	resumed, err := restarted.BeginAuthorization(context.Background(), beginRequest())
	if err != nil || resumed.Outcome != AuthorizationOutcomeConnected || resumed.Principal == nil {
		t.Fatalf("resumed BeginAuthorization = %+v, %v", resumed, err)
	}
	return resumed.Principal
}

func TestAuthorizationSplitsExchangeAndIdentityAcrossRecoverableEntries(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	service := newTestService(repository, provider)
	start, err := service.BeginAuthorization(context.Background(), beginRequest())
	if err != nil {
		t.Fatal(err)
	}
	state := provider.authorizations[0].State
	completion, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code-secret",
	})
	if err != nil || completion.Outcome != AuthorizationCompletionResultStaged {
		t.Fatalf("completion = %+v, %v", completion, err)
	}
	if len(provider.exchanges) != 1 || provider.identityCalls != 0 {
		t.Fatalf("provider calls after Complete = exchange:%d identity:%d", len(provider.exchanges), provider.identityCalls)
	}

	replayed, err := newTestService(repository, provider).CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "ignored",
	})
	if err != nil || replayed.Outcome != AuthorizationCompletionResultStaged || len(provider.exchanges) != 1 {
		t.Fatalf("replayed Complete = %+v, %v calls=%d", replayed, err, len(provider.exchanges))
	}

	resumed, err := newTestService(repository, provider).BeginAuthorization(context.Background(), beginRequest())
	if err != nil || resumed.Principal == nil || resumed.Principal.State != PrincipalConnected {
		t.Fatalf("resumed Begin = %+v, %v", resumed, err)
	}
	if provider.identityCalls != 1 || len(provider.exchanges) != 1 || start.ExpectedGeneration != resumed.Principal.Generation {
		t.Fatalf("final calls/generation = exchange:%d identity:%d generation:%d/%d",
			len(provider.exchanges), provider.identityCalls, start.ExpectedGeneration, resumed.Principal.Generation)
	}
}

func TestAuthorizationPreservesRequestedScopesWhenExchangeOmitsThem(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	service := newTestService(repository, provider)
	start, err := service.BeginAuthorization(t.Context(), beginRequest())
	if err != nil || start.Outcome != AuthorizationOutcomeRedirect {
		t.Fatalf("BeginAuthorization = %+v, %v", start, err)
	}
	provider.tokens.Scopes = nil
	state := provider.authorizations[len(provider.authorizations)-1].State
	completion, err := service.CompleteAuthorization(t.Context(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code-secret",
	})
	if err != nil || completion.Outcome != AuthorizationCompletionResultStaged {
		t.Fatalf("CompleteAuthorization = %+v, %v", completion, err)
	}
	resumed, err := newTestService(repository, provider).BeginAuthorization(t.Context(), beginRequest())
	if err != nil || resumed.Principal == nil || resumed.Principal.State != PrincipalConnected {
		t.Fatalf("resumed BeginAuthorization = %+v, %v", resumed, err)
	}
	token, err := newTestService(repository, provider).ResolveScopedAccessToken(t.Context(), ResolveScopedAccessTokenRequest{
		UserID:             "user-1",
		ConnectorKey:       "provider-a",
		ExpectedGeneration: resumed.Principal.Generation,
		RequiredScopes:     []string{" scope.read ", "scope.read"},
	})
	if err != nil || token != "access-secret" {
		t.Fatalf("ResolveScopedAccessToken = %q, %v", token, err)
	}
}

func TestAuthorizationRejectsExplicitScopeDowngradeBeforeStaging(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	provider.tokens.Scopes = []string{"scope.other"}
	service := newTestService(repository, provider)
	if _, err := service.BeginAuthorization(t.Context(), beginRequest()); err != nil {
		t.Fatal(err)
	}
	state := provider.authorizations[len(provider.authorizations)-1].State
	_, err := service.CompleteAuthorization(t.Context(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code-secret",
	})
	status, statusErr := service.Status(t.Context(), "user-1", "provider-a")
	if !errors.Is(err, ErrScopeMismatch) || !errors.Is(err, ErrManualActionRequired) ||
		statusErr != nil || status.EffectState != EffectManualRequired || provider.identityCalls != 0 {
		t.Fatalf("CompleteAuthorization error/status/identity = %v / %+v, %v / %d",
			err, status, statusErr, provider.identityCalls)
	}
}

func stageAuthorizationScopeResult(
	t *testing.T,
	repository *memoryRepository,
	provider *fakeProvider,
	scopes []string,
) *Service {
	t.Helper()
	service := newTestService(repository, provider)
	if _, err := service.BeginAuthorization(t.Context(), beginRequest()); err != nil {
		t.Fatal(err)
	}

	repository.mu.Lock()
	var digest string
	var record authorizationRecord
	for digest, record = range repository.authorizations {
		break
	}
	repository.mu.Unlock()
	payload, err := marshalPendingTokenSet(TokenSet{
		AccessToken: "staged-access", TokenType: "Bearer", Scopes: scopes,
	})
	if err != nil {
		t.Fatal(err)
	}
	record.OAuthActionEpoch = 1
	ciphertext, _, err := service.cipher.Seal(payload, authorizationResultContext(record, record.OAuthActionEpoch))
	clear(payload)
	if err != nil {
		t.Fatal(err)
	}
	now := time.Now().UTC()
	record.ConsumedAt = &now
	record.OAuthExchangeDispatchedAt = &now
	record.OAuthEffectState = EffectExchangeResultStaged
	record.OAuthEffectVersion++
	record.PendingTokenCiphertext = ciphertext
	repository.mu.Lock()
	repository.authorizations[digest] = record
	repository.mu.Unlock()
	return service
}

func TestStagedAuthorizationRecoversReleasedOmittedScopeFormat(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	service := stageAuthorizationScopeResult(t, repository, provider, nil)

	resumed, err := service.BeginAuthorization(t.Context(), beginRequest())
	if err != nil || resumed.Principal == nil || resumed.Principal.State != PrincipalConnected || provider.identityCalls != 1 {
		t.Fatalf("resumed authorization = %+v, %v; identity=%d", resumed, err, provider.identityCalls)
	}
	unscoped := ResolveAccessTokenRequest{
		UserID: "user-1", ConnectorKey: "provider-a", ExpectedGeneration: resumed.Principal.Generation,
	}
	if token, err := service.ResolveAccessToken(t.Context(), unscoped); err != nil || token != "staged-access" {
		t.Fatalf("ResolveAccessToken = %q, %v", token, err)
	}
	scoped := ResolveScopedAccessTokenRequest{
		UserID: "user-1", ConnectorKey: "provider-a", ExpectedGeneration: resumed.Principal.Generation,
		RequiredScopes: []string{"scope.read"},
	}
	if token, err := service.ResolveScopedAccessToken(t.Context(), scoped); token != "" || !errors.Is(err, ErrScopeMismatch) {
		t.Fatalf("ResolveScopedAccessToken = %q, %v", token, err)
	}
}

func TestStagedAuthorizationRevalidatesRequestedScopesBeforeIdentity(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	service := stageAuthorizationScopeResult(t, repository, provider, []string{"scope.other"})

	_, err := service.BeginAuthorization(t.Context(), beginRequest())
	status, statusErr := service.Status(t.Context(), "user-1", "provider-a")
	if !errors.Is(err, ErrScopeMismatch) || !errors.Is(err, ErrManualActionRequired) ||
		statusErr != nil || status.EffectState != EffectManualRequired || provider.identityCalls != 0 {
		t.Fatalf("resumed authorization error/status/identity = %v / %+v, %v / %d",
			err, status, statusErr, provider.identityCalls)
	}
}

func TestConnectedAuthorizationRequiresDisconnectWithoutProviderCall(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	connectPrincipal(t, repository, provider)
	authorizationCalls := len(provider.authorizations)

	if _, err := newTestService(repository, provider).BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrDisconnectRequired) {
		t.Fatalf("BeginAuthorization error = %v", err)
	}
	if len(provider.authorizations) != authorizationCalls {
		t.Fatalf("reauthorization called provider: %d/%d", len(provider.authorizations), authorizationCalls)
	}
}

func TestDisconnectRejectsGenerationChangedByConcurrentReauthorization(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	newGeneration := principal.Generation + 2
	hooked := disconnectHookRepository{oauthRepository: repository, beforeDisconnect: func() {
		repository.mu.Lock()
		defer repository.mu.Unlock()
		key := accountKey(principal.UserID, principal.ConnectorKey)
		current := repository.principals[key]
		current.Generation = newGeneration
		current.State = PrincipalConnected
		repository.principals[key] = current
		token := repository.tokens[principal.ID]
		token.PrincipalGeneration = newGeneration
		token.TokenVersion++
		repository.tokens[principal.ID] = token
	}}

	err := newTestService(hooked, provider).Disconnect(context.Background(), principal.UserID, principal.ConnectorKey)
	if !errors.Is(err, ErrGenerationMismatch) {
		t.Fatalf("Disconnect error = %v", err)
	}
	repository.mu.Lock()
	retainedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	retainedToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if retainedPrincipal.Generation != newGeneration || retainedPrincipal.State != PrincipalConnected ||
		retainedToken.PrincipalGeneration != newGeneration || provider.revokeCalls != 0 {
		t.Fatalf("retained principal/token/revoke = %+v/%+v/%d", retainedPrincipal, retainedToken, provider.revokeCalls)
	}
}

func TestProviderEffectDeadlineLeavesTwoCleanupWindowsBeforeLeaseExpiry(t *testing.T) {
	provider := testProvider()
	service := newTestService(newMemoryRepository(), provider)
	fixedNow := time.Date(2026, time.July, 18, 12, 0, 0, 0, time.UTC)
	service.now = func() time.Time { return fixedNow }
	leaseExpiresAt := fixedNow.Add(DefaultRefreshLeaseTTL)
	providerCtx, cancel, err := service.providerEffectContext(context.Background(), provider, &leaseExpiresAt)
	if err != nil {
		t.Fatal(err)
	}
	defer cancel()
	deadline, ok := providerCtx.Deadline()
	want := leaseExpiresAt.Add(-provider.EffectTerminationBound() - 2*service.cleanupTimeout)
	if !ok || !deadline.Equal(want) || !deadline.Before(leaseExpiresAt) {
		t.Fatalf("provider deadline = %s/%v, want %s before lease %s", deadline, ok, want, leaseExpiresAt)
	}
}

func TestProviderTerminationBoundLeaseFitBoundaries(t *testing.T) {
	fixedNow := time.Date(2026, time.July, 18, 12, 0, 0, 0, time.UTC)
	for _, test := range []struct {
		name    string
		bound   time.Duration
		wantErr bool
	}{
		{name: "zero", bound: 0, wantErr: true},
		{name: "one nanosecond inside", bound: 79 * time.Nanosecond},
		{name: "exact fit", bound: 80 * time.Nanosecond, wantErr: true},
		{name: "one nanosecond outside", bound: 81 * time.Nanosecond, wantErr: true},
		{name: "maximum duration", bound: time.Duration(1<<63 - 1), wantErr: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			provider := testProvider()
			provider.terminationBound = test.bound
			service := newTestService(newMemoryRepository(), provider)
			service.now = func() time.Time { return fixedNow }
			service.refreshLease = 100 * time.Nanosecond
			service.cleanupTimeout = 10 * time.Nanosecond
			leaseExpiresAt := fixedNow.Add(service.refreshLease)
			providerCtx, cancel, err := service.providerEffectContext(context.Background(), provider, &leaseExpiresAt)
			if cancel != nil {
				cancel()
			}
			if test.wantErr && !errors.Is(err, ErrEffectStateInvalid) {
				t.Fatalf("providerEffectContext error = %v", err)
			}
			if !test.wantErr {
				if err != nil {
					t.Fatal(err)
				}
				deadline, ok := providerCtx.Deadline()
				if !ok || !deadline.Equal(fixedNow.Add(time.Nanosecond)) {
					t.Fatalf("deadline = %s/%v", deadline, ok)
				}
			}
		})
	}
}

func TestSideEffectingProvidersReturnWithinDeclaredCancellationBound(t *testing.T) {
	const lease = 300 * time.Millisecond
	configure := func(service *Service) {
		service.refreshLease = lease
		service.cleanupTimeout = 10 * time.Millisecond
	}

	t.Run("exchange", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		service := newTestService(repository, provider)
		configure(service)
		audit := &fakeCleanupAudit{}
		service.audit = audit
		if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
			t.Fatal(err)
		}
		provider.exchangeStarted = make(chan struct{}, 1)
		provider.exchangeRelease = make(chan struct{})
		provider.terminationBound = 30 * time.Millisecond
		state := provider.authorizations[0].State
		done := make(chan error, 1)
		go func() {
			_, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
				UserID: "user-1", State: state, Code: "code",
			})
			done <- err
		}()
		<-provider.exchangeStarted
		status, err := service.Status(context.Background(), "user-1", "provider-a")
		if err != nil || status.EffectState != EffectExchangeDispatched || status.CanAttest {
			t.Fatalf("Status = %+v, %v", status, err)
		}
		if err := service.AttestExternalCleanup(context.Background(), CleanupAttestation{
			UserID: "user-1", ConnectorKey: "provider-a", Reason: "cleanup confirmed",
		}); !errors.Is(err, ErrAuthorizationProgress) {
			t.Fatalf("AttestExternalCleanup error = %v", err)
		}
		if err := <-done; !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("CompleteAuthorization error = %v", err)
		}
		status, err = service.Status(context.Background(), "user-1", "provider-a")
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
			t.Fatalf("completed Status = %+v, %v", status, err)
		}
		if len(audit.events) != 0 || len(provider.exchanges) != 1 {
			t.Fatalf("audit/exchange calls = %d/%d", len(audit.events), len(provider.exchanges))
		}
	})

	t.Run("refresh", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		provider.refreshStarted = make(chan struct{}, 1)
		provider.refreshRelease = make(chan struct{})
		provider.terminationBound = 30 * time.Millisecond
		service := newTestService(repository, provider)
		configure(service)
		audit := &fakeCleanupAudit{}
		service.audit = audit
		done := make(chan error, 1)
		go func() {
			_, err := service.Refresh(context.Background(), ResolveAccessTokenRequest{
				UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, ExpectedGeneration: principal.Generation,
			})
			done <- err
		}()
		<-provider.refreshStarted
		status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
		if err != nil || status.EffectState != EffectRefreshDispatched || status.CanAttest {
			t.Fatalf("Status = %+v, %v", status, err)
		}
		if err := service.AttestExternalCleanup(context.Background(), CleanupAttestation{
			UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, Reason: "cleanup confirmed",
		}); !errors.Is(err, ErrRefreshInProgress) {
			t.Fatalf("AttestExternalCleanup error = %v", err)
		}
		if err := <-done; !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("Refresh error = %v", err)
		}
		status, err = service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
			t.Fatalf("completed Status = %+v, %v", status, err)
		}
		if len(audit.events) != 0 || provider.refreshCalls != 1 {
			t.Fatalf("audit/refresh calls = %d/%d", len(audit.events), provider.refreshCalls)
		}
	})

	t.Run("revoke", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		provider.revokeStarted = make(chan struct{}, 1)
		provider.revokeRelease = make(chan struct{})
		provider.terminationBound = 30 * time.Millisecond
		service := newTestService(repository, provider)
		configure(service)
		audit := &fakeCleanupAudit{}
		service.audit = audit
		done := make(chan error, 1)
		go func() {
			done <- service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey)
		}()
		<-provider.revokeStarted
		status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
		if err != nil || status.EffectState != EffectRevokeDispatched || status.CanAttest {
			t.Fatalf("Status = %+v, %v", status, err)
		}
		if err := service.AttestExternalCleanup(context.Background(), CleanupAttestation{
			UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, Reason: "cleanup confirmed",
		}); !errors.Is(err, ErrRefreshInProgress) {
			t.Fatalf("AttestExternalCleanup error = %v", err)
		}
		if err := <-done; !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("Disconnect error = %v", err)
		}
		status, err = service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
			t.Fatalf("completed Status = %+v, %v", status, err)
		}
		if len(audit.events) != 0 || provider.revokeCalls != 1 {
			t.Fatalf("audit/revoke calls = %d/%d", len(audit.events), provider.revokeCalls)
		}
	})
}

func TestProviderTerminationBoundMustFitInsideEffectLease(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	provider.terminationBound = 70 * time.Millisecond
	service := newTestService(repository, provider)
	service.refreshLease = 80 * time.Millisecond
	service.cleanupTimeout = 5 * time.Millisecond
	if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
		t.Fatal(err)
	}
	state := provider.authorizations[0].State
	if _, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code",
	}); !errors.Is(err, ErrEffectStateInvalid) {
		t.Fatalf("CompleteAuthorization error = %v", err)
	}
	if len(provider.exchanges) != 0 {
		t.Fatalf("Exchange calls = %d", len(provider.exchanges))
	}
}

func TestAuthorizationProviderFailuresRequireManualAction(t *testing.T) {
	for _, kind := range []ProviderErrorKind{
		ProviderErrorAuthorization, ProviderErrorRejected, ProviderErrorTransient, ProviderErrorUncertain,
	} {
		t.Run(string(kind), func(t *testing.T) {
			repository := newMemoryRepository()
			provider := testProvider()
			provider.exchangeErr = &ProviderError{Operation: "exchange", Kind: kind, Code: "provider_failure"}
			service := newTestService(repository, provider)
			if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
				t.Fatal(err)
			}
			state := provider.authorizations[0].State
			_, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{UserID: "user-1", State: state, Code: "code"})
			if !errors.Is(err, ErrManualActionRequired) {
				t.Fatalf("CompleteAuthorization error = %v", err)
			}
			status, err := service.Status(context.Background(), "user-1", "provider-a")
			if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
				t.Fatalf("Status = %+v, %v", status, err)
			}
			if _, err := newTestService(repository, provider).CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{UserID: "user-1", State: state, Code: "code"}); !errors.Is(err, ErrManualActionRequired) {
				t.Fatalf("replay error = %v", err)
			}
			if len(provider.exchanges) != 1 || provider.identityCalls != 0 {
				t.Fatalf("provider calls = exchange:%d identity:%d", len(provider.exchanges), provider.identityCalls)
			}
		})
	}
}

func TestAuthorizationPostExchangeFailuresConvergeSafely(t *testing.T) {
	t.Run("result encryption", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		service := newTestService(repository, provider)
		if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
			t.Fatal(err)
		}
		state := provider.authorizations[0].State
		service.cipher = purposeFailCipher{SecretCipher: service.cipher, sealPurpose: SecretAuthorizationResult,
			err: ErrEncryptionKeyMissing}
		if _, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
			UserID: "user-1", State: state, Code: "code",
		}); !errors.Is(err, ErrEncryptionKeyMissing) || !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("CompleteAuthorization error = %v", err)
		}
		status, err := service.Status(context.Background(), "user-1", "provider-a")
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
			t.Fatalf("Status = %+v, %v", status, err)
		}
	})

	for _, commitBeforeError := range []bool{false, true} {
		name := "stage not committed"
		if commitBeforeError {
			name = "stage committed"
		}
		t.Run(name, func(t *testing.T) {
			repository := newMemoryRepository()
			repository.stageAuthorizationErr = errors.New("stage unavailable")
			repository.stageAuthorizationCommitBeforeErr = commitBeforeError
			provider := testProvider()
			service := newTestService(repository, provider)
			if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
				t.Fatal(err)
			}
			state := provider.authorizations[0].State
			completion, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
				UserID: "user-1", State: state, Code: "code",
			})
			status, statusErr := service.Status(context.Background(), "user-1", "provider-a")
			if commitBeforeError {
				if err != nil || completion.Outcome != AuthorizationCompletionResultStaged || statusErr != nil ||
					status.EffectState != EffectExchangeResultStaged || status.CanAttest {
					t.Fatalf("completion/status = %+v, %v / %+v, %v", completion, err, status, statusErr)
				}
				return
			}
			if !errors.Is(err, ErrManualActionRequired) || statusErr != nil ||
				status.EffectState != EffectManualRequired || !status.CanAttest {
				t.Fatalf("completion/status = %+v, %v / %+v, %v", completion, err, status, statusErr)
			}
		})
	}

	t.Run("stage committed with different payload", func(t *testing.T) {
		repository := newMemoryRepository()
		repository.stageAuthorizationErr = errors.New("stage unavailable")
		repository.stageAuthorizationCommitBeforeErr = true
		repository.stageAuthorizationCommittedCiphertext = "different-staged-result"
		provider := testProvider()
		service := newTestService(repository, provider)
		if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
			t.Fatal(err)
		}
		state := provider.authorizations[0].State
		if _, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
			UserID: "user-1", State: state, Code: "code",
		}); !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("CompleteAuthorization error = %v", err)
		}
		status, err := service.Status(context.Background(), "user-1", "provider-a")
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
			t.Fatalf("Status = %+v, %v", status, err)
		}
		if _, err := service.BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("BeginAuthorization error = %v", err)
		}
		if len(provider.exchanges) != 1 {
			t.Fatalf("Exchange calls = %d", len(provider.exchanges))
		}
	})
}

func TestClaimedAuthorizationWithDispatchEvidenceFailsClosedWithoutExchange(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	service := newTestService(repository, provider)
	if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
		t.Fatal(err)
	}
	state := provider.authorizations[0].State
	repository.mu.Lock()
	for digest, record := range repository.authorizations {
		expired := time.Now().UTC().Add(-time.Second)
		dispatched := time.Now().UTC().Add(-time.Minute)
		record.OAuthEffectState = EffectExchangeClaimed
		record.OAuthEffectVersion++
		record.OAuthActionOwnerID = "dead-process"
		record.OAuthActionEpoch++
		record.OAuthActionLeaseExpiresAt = &expired
		record.OAuthExchangeDispatchedAt = &dispatched
		repository.authorizations[digest] = record
	}
	repository.mu.Unlock()

	if _, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code",
	}); !errors.Is(err, ErrEffectStateInvalid) {
		t.Fatalf("CompleteAuthorization error = %v", err)
	}
	if len(provider.exchanges) != 0 {
		t.Fatalf("Exchange calls = %d", len(provider.exchanges))
	}
}

func TestClaimedRevokeWithDispatchEvidenceFailsClosedWithoutRevoke(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	storedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	storedPrincipal.State = PrincipalReconnectRequired
	repository.principals[accountKey(principal.UserID, principal.ConnectorKey)] = storedPrincipal
	token := repository.tokens[principal.ID]
	expired := time.Now().UTC().Add(-time.Second)
	dispatched := time.Now().UTC().Add(-time.Minute)
	token.OAuthEffectState = EffectRevokeClaimed
	token.OAuthEffectVersion++
	token.RefreshOwnershipEpoch++
	token.RefreshOwnerID = "dead-process"
	token.RefreshLeaseExpiresAt = &expired
	token.RefreshDispatchStartedAt = &dispatched
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	if err := newTestService(repository, provider).Revoke(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrEffectStateInvalid) {
		t.Fatalf("Revoke error = %v", err)
	}
	if provider.revokeCalls != 0 {
		t.Fatalf("provider Revoke calls = %d", provider.revokeCalls)
	}
}

func TestResolveAccessTokenRejectsContradictoryEffectFields(t *testing.T) {
	for _, test := range []struct {
		name  string
		state string
		setup func(*tokenRecord)
	}{
		{name: "none with pending result", state: EffectNone},
		{name: "expired claim with pending result", state: EffectRefreshClaimed, setup: func(token *tokenRecord) {
			expired := time.Now().UTC().Add(-time.Second)
			token.RefreshOwnerID = "dead-process"
			token.RefreshLeaseExpiresAt = &expired
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			repository := newMemoryRepository()
			provider := testProvider()
			principal := connectPrincipal(t, repository, provider)
			repository.mu.Lock()
			token := repository.tokens[principal.ID]
			token.OAuthEffectState = test.state
			token.PendingTokenCiphertext = "unexpected"
			if test.setup != nil {
				test.setup(&token)
			}
			repository.tokens[principal.ID] = token
			repository.mu.Unlock()

			request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
				ExpectedGeneration: principal.Generation}
			if _, err := newTestService(repository, provider).ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrEffectStateInvalid) {
				t.Fatalf("ResolveAccessToken error = %v", err)
			}
		})
	}
}

func TestResolveScopedAccessTokenRequiresCommittedGrantedScopes(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	service := newTestService(repository, provider)
	base := ResolveScopedAccessTokenRequest{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation,
	}

	allowed := base
	allowed.RequiredScopes = []string{"scope.read"}
	if token, err := service.ResolveScopedAccessToken(t.Context(), allowed); err != nil || token != "access-secret" {
		t.Fatalf("allowed token = %q, %v", token, err)
	}

	denied := base
	denied.RequiredScopes = []string{"mail.send"}
	if token, err := service.ResolveScopedAccessToken(t.Context(), denied); token != "" || !errors.Is(err, ErrScopeMismatch) {
		t.Fatalf("denied token = %q, %v", token, err)
	}

	if token, err := service.ResolveScopedAccessToken(t.Context(), base); token != "" || !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("empty-scope token = %q, %v", token, err)
	}
	if provider.refreshCalls != 0 {
		t.Fatalf("scope rejection called Refresh %d times", provider.refreshCalls)
	}
}

func TestScopedRefreshPreservesCommittedScopesWhenProviderOmitsThem(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	provider.refreshed.Scopes = nil
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}

	if token, err := newTestService(repository, provider).Refresh(t.Context(), request); token != "" || !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("Refresh = %q, %v", token, err)
	}
	scoped := ResolveScopedAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation, RequiredScopes: []string{"scope.read"}}
	token, resolveErr := newTestService(repository, provider).ResolveScopedAccessToken(t.Context(), scoped)
	repository.mu.Lock()
	stored := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if resolveErr != nil || token != "access-refreshed" || stored.GrantedScopes != "scope.read" {
		t.Fatalf("scoped refresh = %q, %v; grants = %q", token, resolveErr, stored.GrantedScopes)
	}
}

func TestRefreshRejectsExplicitScopeDowngradeBeforeReplacingCommittedToken(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	provider.refreshed.Scopes = []string{"scope.other"}
	repository.mu.Lock()
	beforePrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	beforeToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}

	_, err := newTestService(repository, provider).Refresh(t.Context(), request)
	repository.mu.Lock()
	afterPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	afterToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if !errors.Is(err, ErrScopeMismatch) || !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("Refresh error = %v", err)
	}
	if beforePrincipal.State != PrincipalConnected || afterPrincipal.State != PrincipalReconnectRequired ||
		afterToken.AccessTokenCiphertext != beforeToken.AccessTokenCiphertext ||
		afterToken.RefreshTokenCiphertext != beforeToken.RefreshTokenCiphertext ||
		afterToken.GrantedScopes != beforeToken.GrantedScopes || afterToken.OAuthEffectState != EffectManualRequired {
		t.Fatalf("scope downgrade states=%q/%q effect=%q access_unchanged=%t refresh_unchanged=%t grants=%q/%q",
			beforePrincipal.State, afterPrincipal.State, afterToken.OAuthEffectState,
			afterToken.AccessTokenCiphertext == beforeToken.AccessTokenCiphertext,
			afterToken.RefreshTokenCiphertext == beforeToken.RefreshTokenCiphertext,
			beforeToken.GrantedScopes, afterToken.GrantedScopes)
	}
}

func stageRefreshScopeResult(
	t *testing.T,
	repository *memoryRepository,
	principal *ConnectedPrincipal,
	service *Service,
	scopes []string,
) tokenRecord {
	t.Helper()
	repository.mu.Lock()
	beforeToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	payload, err := marshalPendingTokenSet(TokenSet{
		AccessToken: "staged-access", RefreshToken: "staged-refresh", TokenType: "Bearer",
		Scopes: scopes,
	})
	if err != nil {
		t.Fatal(err)
	}
	ciphertext, _, err := service.cipher.Seal(payload, refreshResultContext(*principal, beforeToken))
	clear(payload)
	if err != nil {
		t.Fatal(err)
	}
	dispatchedAt := time.Now().UTC()
	staged := beforeToken
	staged.OAuthEffectState = EffectRefreshResultStaged
	staged.OAuthEffectVersion++
	staged.RefreshOwnerID = ""
	staged.RefreshLeaseExpiresAt = nil
	staged.RefreshDispatchStartedAt = &dispatchedAt
	staged.PendingTokenCiphertext = ciphertext
	repository.mu.Lock()
	repository.tokens[principal.ID] = staged
	repository.mu.Unlock()
	return beforeToken
}

func TestStagedRefreshRecoversReleasedOmittedScopeFormat(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	service := newTestService(repository, provider)
	stageRefreshScopeResult(t, repository, principal, service, nil)

	request := ResolveAccessTokenRequest{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation,
	}
	accessToken, err := service.ResolveAccessToken(t.Context(), request)
	repository.mu.Lock()
	stored := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if err != nil || accessToken != "staged-access" || stored.GrantedScopes != "" ||
		stored.OAuthEffectState != EffectNone || provider.refreshCalls != 0 {
		t.Fatalf("released staged refresh = %q, %v; grants=%q effect=%q refresh=%d",
			accessToken, err, stored.GrantedScopes, stored.OAuthEffectState, provider.refreshCalls)
	}
	scoped := ResolveScopedAccessTokenRequest{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation, RequiredScopes: []string{"scope.read"},
	}
	if token, err := service.ResolveScopedAccessToken(t.Context(), scoped); token != "" || !errors.Is(err, ErrScopeMismatch) {
		t.Fatalf("ResolveScopedAccessToken = %q, %v", token, err)
	}
}

func TestStagedRefreshCallerScopeMismatchPreservesHealthyResult(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	service := newTestService(repository, provider)
	beforeToken := stageRefreshScopeResult(t, repository, principal, service, []string{"scope.read"})
	repository.mu.Lock()
	stagedToken := repository.tokens[principal.ID]
	repository.mu.Unlock()

	scoped := ResolveScopedAccessTokenRequest{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation, RequiredScopes: []string{"mail.send"},
	}
	if token, err := service.ResolveScopedAccessToken(t.Context(), scoped); token != "" || !errors.Is(err, ErrScopeMismatch) {
		t.Fatalf("ResolveScopedAccessToken = %q, %v", token, err)
	}
	repository.mu.Lock()
	afterMismatchPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	afterMismatchToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if afterMismatchPrincipal.State != PrincipalConnected || afterMismatchToken.OAuthEffectState != EffectRefreshResultStaged ||
		afterMismatchToken.PendingTokenCiphertext != stagedToken.PendingTokenCiphertext ||
		afterMismatchToken.AccessTokenCiphertext != beforeToken.AccessTokenCiphertext || provider.refreshCalls != 0 {
		t.Fatalf("scope mismatch mutated principal/token/refresh = %+v / %+v / %d",
			afterMismatchPrincipal, afterMismatchToken, provider.refreshCalls)
	}

	unscoped := ResolveAccessTokenRequest{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, ExpectedGeneration: principal.Generation,
	}
	accessToken, err := service.ResolveAccessToken(t.Context(), unscoped)
	repository.mu.Lock()
	recovered := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if err != nil || accessToken != "staged-access" || recovered.OAuthEffectState != EffectNone ||
		recovered.GrantedScopes != "scope.read" || provider.refreshCalls != 0 {
		t.Fatalf("unscoped recovery = %q, %v; token=%+v refresh=%d",
			accessToken, err, recovered, provider.refreshCalls)
	}
}

func TestStagedRefreshRevalidatesScopesBeforeReplacingCommittedToken(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	service := newTestService(repository, provider)
	beforeToken := stageRefreshScopeResult(t, repository, principal, service, []string{"scope.other"})

	request := ResolveScopedAccessTokenRequest{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation, RequiredScopes: []string{"scope.read"},
	}
	_, err := service.ResolveScopedAccessToken(t.Context(), request)
	repository.mu.Lock()
	afterPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	afterToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if !errors.Is(err, ErrScopeMismatch) || !errors.Is(err, ErrManualActionRequired) ||
		afterPrincipal.State != PrincipalReconnectRequired || afterToken.OAuthEffectState != EffectManualRequired ||
		afterToken.AccessTokenCiphertext != beforeToken.AccessTokenCiphertext ||
		afterToken.RefreshTokenCiphertext != beforeToken.RefreshTokenCiphertext ||
		afterToken.GrantedScopes != beforeToken.GrantedScopes || provider.refreshCalls != 0 {
		t.Fatalf("staged downgrade error/principal/token/refresh = %v / %+v / %+v / %d",
			err, afterPrincipal, afterToken, provider.refreshCalls)
	}
}

func TestMalformedStableEffectShapesFailClosedAtEntrypoints(t *testing.T) {
	t.Run("authorization reservation", func(t *testing.T) {
		for _, state := range []string{EffectNone, EffectExchangeResultStaged} {
			repository := newMemoryRepository()
			provider := testProvider()
			service := newTestService(repository, provider)
			if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
				t.Fatal(err)
			}
			repository.mu.Lock()
			for digest, record := range repository.authorizations {
				record.ExpiresAt = time.Now().UTC().Add(-time.Second)
				record.OAuthEffectState = state
				record.PendingTokenCiphertext = "unexpected"
				record.OAuthExchangeDispatchedAt = nil
				repository.authorizations[digest] = record
			}
			repository.mu.Unlock()
			authorizationCalls := len(provider.authorizations)
			if _, err := service.BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrEffectStateInvalid) {
				t.Fatalf("BeginAuthorization state %s error = %v", state, err)
			}
			if len(provider.authorizations) != authorizationCalls {
				t.Fatalf("AuthorizationURL calls = %d, want %d", len(provider.authorizations), authorizationCalls)
			}
		}
		for _, test := range []struct {
			name  string
			state string
			setup func(*authorizationRecord)
		}{
			{name: "unconsumed claimed", state: EffectExchangeClaimed, setup: func(record *authorizationRecord) {
				expired := time.Now().UTC().Add(-time.Second)
				record.OAuthActionOwnerID = "dead-process"
				record.OAuthActionLeaseExpiresAt = &expired
			}},
			{name: "consumed none", state: EffectNone, setup: func(record *authorizationRecord) {
				consumed := time.Now().UTC().Add(-time.Minute)
				record.ConsumedAt = &consumed
			}},
		} {
			t.Run(test.name, func(t *testing.T) {
				repository := newMemoryRepository()
				provider := testProvider()
				service := newTestService(repository, provider)
				if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
					t.Fatal(err)
				}
				repository.mu.Lock()
				for digest, record := range repository.authorizations {
					record.ExpiresAt = time.Now().UTC().Add(-time.Second)
					record.OAuthEffectState = test.state
					test.setup(&record)
					repository.authorizations[digest] = record
				}
				repository.mu.Unlock()
				if _, err := service.BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrEffectStateInvalid) {
					t.Fatalf("BeginAuthorization error = %v", err)
				}
				if len(repository.authorizations) != 1 {
					t.Fatalf("authorization count = %d", len(repository.authorizations))
				}
			})
		}
	})

	t.Run("direct staged refresh", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		repository.mu.Lock()
		token := repository.tokens[principal.ID]
		dispatched := time.Now().UTC()
		token.OAuthEffectState = EffectRefreshResultStaged
		token.PendingTokenCiphertext = "unexpected"
		token.RefreshDispatchStartedAt = &dispatched
		token.RefreshOwnerID = "stale-owner"
		repository.tokens[principal.ID] = token
		repository.mu.Unlock()
		request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
			ExpectedGeneration: principal.Generation}
		if _, err := newTestService(repository, provider).Refresh(context.Background(), request); !errors.Is(err, ErrEffectStateInvalid) {
			t.Fatalf("Refresh error = %v", err)
		}
		if provider.refreshCalls != 0 {
			t.Fatalf("Refresh calls = %d", provider.refreshCalls)
		}
	})

	t.Run("revoke succeeded deletion", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		repository.mu.Lock()
		storedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
		storedPrincipal.State = PrincipalDisconnected
		repository.principals[accountKey(principal.UserID, principal.ConnectorKey)] = storedPrincipal
		token := repository.tokens[principal.ID]
		token.OAuthEffectState = EffectRevokeSucceeded
		token.RefreshDispatchStartedAt = nil
		repository.tokens[principal.ID] = token
		repository.mu.Unlock()
		if _, err := repository.claimRevoke(context.Background(), principal.UserID, principal.ConnectorKey,
			"new-owner", time.Now().UTC(), time.Now().UTC().Add(time.Minute)); !errors.Is(err, ErrEffectStateInvalid) {
			t.Fatalf("claimRevoke error = %v", err)
		}
		if err := repository.deleteRevokedToken(context.Background(), principal.UserID, principal.ConnectorKey,
			principal.ID, token.PrincipalGeneration, token.TokenVersion); !errors.Is(err, ErrEffectStateInvalid) {
			t.Fatalf("deleteRevokedToken error = %v", err)
		}
		repository.mu.Lock()
		_, retained := repository.tokens[principal.ID]
		repository.mu.Unlock()
		if !retained || provider.revokeCalls != 0 {
			t.Fatalf("retained/revoke calls = %v/%d", retained, provider.revokeCalls)
		}
	})

	t.Run("disconnect claimed refresh with dispatch evidence", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		repository.mu.Lock()
		storedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
		storedPrincipal.State = PrincipalDisconnected
		repository.principals[accountKey(principal.UserID, principal.ConnectorKey)] = storedPrincipal
		token := repository.tokens[principal.ID]
		dispatched := time.Now().UTC()
		lease := dispatched.Add(time.Minute)
		token.OAuthEffectState = EffectRefreshClaimed
		token.RefreshOwnerID = "effect-owner"
		token.RefreshLeaseExpiresAt = &lease
		token.RefreshDispatchStartedAt = &dispatched
		repository.tokens[principal.ID] = token
		repository.mu.Unlock()
		if err := newTestService(repository, provider).Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrEffectStateInvalid) {
			t.Fatalf("Disconnect error = %v", err)
		}
		repository.mu.Lock()
		retainedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
		storedToken := repository.tokens[principal.ID]
		repository.mu.Unlock()
		if retainedPrincipal.State != PrincipalDisconnected || retainedPrincipal.Generation != storedPrincipal.Generation ||
			storedToken.OAuthEffectState != EffectRefreshClaimed || storedToken.OAuthEffectVersion != token.OAuthEffectVersion ||
			storedToken.RefreshOwnershipEpoch != token.RefreshOwnershipEpoch || storedToken.RefreshOwnerID != token.RefreshOwnerID ||
			storedToken.RefreshLeaseExpiresAt == nil || !storedToken.RefreshLeaseExpiresAt.Equal(lease) ||
			storedToken.RefreshDispatchStartedAt == nil || !storedToken.RefreshDispatchStartedAt.Equal(dispatched) ||
			provider.revokeCalls != 0 {
			t.Fatalf("retained principal/token/revoke = %+v/%+v/%d", retainedPrincipal, storedToken, provider.revokeCalls)
		}
	})
}

func TestInvalidIdentityMovesStagedAuthorizationToAttestableManualState(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	service := newTestService(repository, provider)
	if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
		t.Fatal(err)
	}
	state := provider.authorizations[0].State
	if _, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
		UserID: "user-1", State: state, Code: "code-secret",
	}); err != nil {
		t.Fatal(err)
	}
	provider.identity = RemoteIdentity{}
	service = newTestService(repository, provider)
	if _, err := service.BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("identity recovery error = %v", err)
	}
	status, err := service.Status(context.Background(), "user-1", "provider-a")
	if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
		t.Fatalf("manual status = %+v, %v", status, err)
	}
	if len(provider.exchanges) != 1 || provider.identityCalls != 1 {
		t.Fatalf("provider calls = exchange:%d identity:%d", len(provider.exchanges), provider.identityCalls)
	}
}

func TestRefreshStagesThenFreshServiceFinalizesWithoutSecondProviderCall(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}

	if token, err := newTestService(repository, provider).Refresh(context.Background(), request); token != "" || !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("first Refresh = %q, %v", token, err)
	}
	if provider.refreshCalls != 1 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	token, err := newTestService(repository, provider).ResolveAccessToken(context.Background(), request)
	if err != nil || token != "access-refreshed" {
		t.Fatalf("recovered ResolveAccessToken = %q, %v", token, err)
	}
	if provider.refreshCalls != 1 {
		t.Fatalf("recovery repeated provider call: %d", provider.refreshCalls)
	}
}

func TestRefreshResultSealFailureRequiresAttestationWithoutRevokingOldToken(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	service := newTestService(repository, provider)
	service.cipher = purposeFailCipher{SecretCipher: service.cipher, sealPurpose: SecretRefreshResult,
		err: ErrEncryptionKeyMissing}
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}

	if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("Refresh error = %v", err)
	}
	status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if err != nil || status.PrincipalState != PrincipalReconnectRequired || status.EffectState != EffectManualRequired || !status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, err)
	}
	if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("Disconnect error = %v", err)
	}
	if provider.refreshCalls != 1 || provider.revokeCalls != 0 {
		t.Fatalf("provider calls = refresh:%d revoke:%d", provider.refreshCalls, provider.revokeCalls)
	}
}

func TestUncertainRefreshRequiresCleanupAttestationWithoutRevoke(t *testing.T) {
	for _, test := range []struct {
		name string
		err  error
	}{
		{name: "classified uncertain", err: &ProviderError{Operation: "refresh", Kind: ProviderErrorUncertain, Code: "timeout"}},
		{name: "unclassified", err: errors.New("connection reset")},
		{name: "typed nil", err: (*ProviderError)(nil)},
	} {
		t.Run(test.name, func(t *testing.T) {
			repository := newMemoryRepository()
			provider := testProvider()
			principal := connectPrincipal(t, repository, provider)
			provider.refreshErr = test.err
			request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
				ExpectedGeneration: principal.Generation}
			service := newTestService(repository, provider)
			if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrManualActionRequired) {
				t.Fatalf("Refresh error = %v", err)
			}
			if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrManualActionRequired) {
				t.Fatalf("Disconnect error = %v", err)
			}
			status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
			if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest {
				t.Fatalf("Status = %+v, %v", status, err)
			}
			audit := &fakeCleanupAudit{}
			service.audit = audit
			if err := service.AttestExternalCleanup(context.Background(), CleanupAttestation{
				UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, Reason: "confirmed remote cleanup",
			}); err != nil {
				t.Fatalf("AttestExternalCleanup: %v", err)
			}
			if provider.refreshCalls != 1 || provider.revokeCalls != 0 || len(audit.events) != 1 {
				t.Fatalf("provider/audit calls = refresh:%d revoke:%d audit:%d", provider.refreshCalls, provider.revokeCalls, len(audit.events))
			}
		})
	}
}

func TestRefreshStageAndReconciliationFailureDisconnectsToManualWithoutRevoke(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.stageRefreshErr = errors.New("stage unavailable")
	repository.reconcileRefreshErr = errors.New("reconciliation unavailable")
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	service := newTestService(repository, provider)
	if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("Refresh error = %v", err)
	}
	if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("Disconnect error = %v", err)
	}
	status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if err != nil || status.PrincipalState != PrincipalDisconnected || status.EffectState != EffectManualRequired || !status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, err)
	}
	if provider.refreshCalls != 1 || provider.revokeCalls != 0 {
		t.Fatalf("provider calls = refresh:%d revoke:%d", provider.refreshCalls, provider.revokeCalls)
	}
}

func TestRefreshStageCommitOutcomeIsReconciled(t *testing.T) {
	for _, test := range []struct {
		name         string
		commit       bool
		wantEffect   string
		wantManual   bool
		wantResolved bool
	}{
		{name: "not committed", wantEffect: EffectManualRequired, wantManual: true},
		{name: "committed before error", commit: true, wantEffect: EffectRefreshResultStaged, wantResolved: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			repository := newMemoryRepository()
			provider := testProvider()
			principal := connectPrincipal(t, repository, provider)
			repository.stageRefreshErr = errors.New("stage result unavailable")
			repository.stageRefreshCommitBeforeErr = test.commit
			request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
				ExpectedGeneration: principal.Generation}
			_, err := newTestService(repository, provider).Refresh(context.Background(), request)
			if !errors.Is(err, ErrEffectResultStaged) && !(test.wantManual && errors.Is(err, ErrManualActionRequired)) {
				t.Fatalf("Refresh error = %v", err)
			}
			status, statusErr := newTestService(repository, provider).Status(context.Background(), principal.UserID, principal.ConnectorKey)
			if statusErr != nil || status.EffectState != test.wantEffect || status.CanAttest != test.wantManual {
				t.Fatalf("Status = %+v, %v", status, statusErr)
			}
			if test.wantResolved {
				token, resolveErr := newTestService(repository, provider).ResolveAccessToken(context.Background(), request)
				if resolveErr != nil || token != "access-refreshed" {
					t.Fatalf("ResolveAccessToken = %q, %v", token, resolveErr)
				}
			}
			if provider.refreshCalls != 1 {
				t.Fatalf("refresh calls = %d", provider.refreshCalls)
			}
		})
	}
}

func TestRefreshStageReconciliationRejectsDifferentPayload(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.stageRefreshErr = errors.New("stage result unavailable")
	repository.stageRefreshCommitBeforeErr = true
	repository.stageRefreshCommittedCiphertext = "different-staged-result"
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	_, err := newTestService(repository, provider).Refresh(context.Background(), request)
	if !errors.Is(err, ErrManualActionRequired) || errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("Refresh error = %v", err)
	}
	status, statusErr := newTestService(repository, provider).Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if statusErr != nil || status.PrincipalState != PrincipalReconnectRequired ||
		status.EffectState != EffectManualRequired || !status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, statusErr)
	}
}

func TestRefreshFinalizesStagedResultWithoutProviderAvailability(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	if _, err := newTestService(repository, provider).Refresh(context.Background(), request); !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("initial Refresh error = %v", err)
	}
	accessToken, err := newTestService(repository, nil).Refresh(context.Background(), request)
	if err != nil || accessToken != "access-refreshed" {
		t.Fatalf("recovery Refresh = %q, %v", accessToken, err)
	}
	if provider.refreshCalls != 1 {
		t.Fatalf("Refresh calls = %d", provider.refreshCalls)
	}
}

func TestRefreshDispatchedWithoutResultRemainsBlockedWithoutRetry(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	marker := time.Now().Add(-time.Minute)
	leaseExpiredAt := time.Now().Add(-time.Second)
	token.OAuthEffectState = EffectRefreshDispatched
	token.RefreshOwnerID = "dead-process"
	token.RefreshLeaseExpiresAt = &leaseExpiredAt
	token.RefreshDispatchStartedAt = &marker
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	service := newTestService(repository, provider)
	if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("Refresh error = %v", err)
	}
	if provider.refreshCalls != 0 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	repository.mu.Lock()
	if repository.tokens[principal.ID].OAuthEffectState != EffectRefreshDispatched ||
		repository.principals[accountKey(principal.UserID, principal.ConnectorKey)].State != PrincipalConnected {
		t.Fatalf("fail-closed state = principal:%+v token:%+v",
			repository.principals[accountKey(principal.UserID, principal.ConnectorKey)], repository.tokens[principal.ID])
	}
	repository.mu.Unlock()
	if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("Disconnect error = %v", err)
	}
	status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if err != nil || status.EffectState != EffectRefreshDispatched || !status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, err)
	}
	audit := &fakeCleanupAudit{}
	service.audit = audit
	if err := service.AttestExternalCleanup(context.Background(), CleanupAttestation{
		UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, Reason: "cleanup confirmed",
	}); err != nil {
		t.Fatalf("AttestExternalCleanup error = %v", err)
	}
	repository.mu.Lock()
	retainedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	_, retainedToken := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if retainedToken || retainedPrincipal.State != PrincipalReconnectRequired ||
		retainedPrincipal.Generation != principal.Generation+1 || len(audit.events) != 1 ||
		audit.events[0].PreviousEffectState != EffectRefreshDispatched {
		t.Fatalf("attested principal/token/audit = %+v/%v/%+v", retainedPrincipal, retainedToken, audit.events)
	}
	if provider.revokeCalls != 0 {
		t.Fatalf("Disconnect called Revoke: %d", provider.revokeCalls)
	}
}

func TestResolveAccessTokenRecoversExpiredPreDispatchRefreshClaim(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	expiredAt := time.Now().UTC().Add(-time.Minute)
	leaseExpiredAt := time.Now().UTC().Add(-time.Second)
	token.ExpiresAt = &expiredAt
	token.OAuthEffectState = EffectRefreshClaimed
	token.OAuthEffectVersion++
	token.RefreshOwnershipEpoch++
	token.RefreshOwnerID = "dead-process"
	token.RefreshLeaseExpiresAt = &leaseExpiredAt
	token.RefreshDispatchStartedAt = nil
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	service := newTestService(repository, provider)
	if _, err := service.ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("first ResolveAccessToken error = %v", err)
	}
	resolved, err := service.ResolveAccessToken(context.Background(), request)
	if err != nil || resolved != "access-refreshed" {
		t.Fatalf("second ResolveAccessToken = %q, %v", resolved, err)
	}
	if provider.refreshCalls != 1 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	repository.mu.Lock()
	defer repository.mu.Unlock()
	stored := repository.tokens[principal.ID]
	if stored.OAuthEffectState != EffectNone || stored.RefreshOwnerID != "" || stored.RefreshLeaseExpiresAt != nil {
		t.Fatalf("recovered token state = %+v", stored)
	}
}

func TestConcurrentRefreshKeepsActiveDispatchedEffectInProgress(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	dispatchedAt := time.Now().UTC()
	leaseExpiresAt := dispatchedAt.Add(time.Minute)
	token.OAuthEffectState = EffectRefreshDispatched
	token.OAuthEffectVersion++
	token.RefreshOwnershipEpoch++
	token.RefreshOwnerID = "active-process"
	token.RefreshLeaseExpiresAt = &leaseExpiresAt
	token.RefreshDispatchStartedAt = &dispatchedAt
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	if _, err := newTestService(repository, provider).Refresh(context.Background(), request); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("Refresh error = %v", err)
	}
	if provider.refreshCalls != 0 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	repository.mu.Lock()
	defer repository.mu.Unlock()
	storedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	storedToken := repository.tokens[principal.ID]
	if storedPrincipal.State != PrincipalConnected || storedToken.OAuthEffectState != EffectRefreshDispatched ||
		storedToken.RefreshOwnerID != "active-process" || storedToken.RefreshLeaseExpiresAt == nil || !storedToken.RefreshLeaseExpiresAt.Equal(leaseExpiresAt) {
		t.Fatalf("active dispatch was mutated: principal=%+v token=%+v", storedPrincipal, storedToken)
	}
}

func TestDisconnectCannotBypassInFlightRefresh(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	provider.refreshStarted = make(chan struct{}, 1)
	provider.refreshRelease = make(chan struct{})
	principal := connectPrincipal(t, repository, provider)
	service := newTestService(repository, provider)
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	done := make(chan error, 1)
	go func() {
		_, err := service.Refresh(context.Background(), request)
		done <- err
	}()
	<-provider.refreshStarted

	if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("Disconnect error = %v", err)
	}
	status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if err != nil || status.PrincipalState != PrincipalConnected || status.EffectState != EffectRefreshDispatched || status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, err)
	}
	close(provider.refreshRelease)
	if err := <-done; !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("Refresh error = %v", err)
	}
	resolved, err := service.ResolveAccessToken(context.Background(), request)
	if err != nil || resolved != "access-refreshed" {
		t.Fatalf("ResolveAccessToken = %q, %v", resolved, err)
	}
}

func TestResolveAccessTokenKeepsExpiredDispatchedRefreshBlocked(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	dispatchedAt := time.Now().UTC().Add(-time.Minute)
	leaseExpiredAt := time.Now().UTC().Add(-time.Second)
	token.OAuthEffectState = EffectRefreshDispatched
	token.OAuthEffectVersion++
	token.RefreshOwnershipEpoch++
	token.RefreshOwnerID = "dead-process"
	token.RefreshLeaseExpiresAt = &leaseExpiredAt
	token.RefreshDispatchStartedAt = &dispatchedAt
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	if _, err := newTestService(repository, provider).ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("ResolveAccessToken error = %v", err)
	}
	if provider.refreshCalls != 0 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	repository.mu.Lock()
	defer repository.mu.Unlock()
	storedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	storedToken := repository.tokens[principal.ID]
	if storedPrincipal.State != PrincipalConnected || storedToken.OAuthEffectState != EffectRefreshDispatched ||
		storedToken.RefreshOwnerID != "dead-process" {
		t.Fatalf("expired dispatch was mutated: principal=%+v token=%+v", storedPrincipal, storedToken)
	}
}

func TestActivePreDispatchRefreshClaimRemainsInProgressAcrossEntries(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	leaseExpiresAt := time.Now().UTC().Add(time.Minute)
	token.OAuthEffectState = EffectRefreshClaimed
	token.OAuthEffectVersion++
	token.RefreshOwnershipEpoch++
	token.RefreshOwnerID = "active-process"
	token.RefreshLeaseExpiresAt = &leaseExpiresAt
	token.RefreshDispatchStartedAt = nil
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	service := newTestService(repository, provider)
	if _, err := service.ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("ResolveAccessToken error = %v", err)
	}
	if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrRefreshInProgress) {
		t.Fatalf("Refresh error = %v", err)
	}
	if provider.refreshCalls != 0 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	repository.mu.Lock()
	defer repository.mu.Unlock()
	stored := repository.tokens[principal.ID]
	if stored.OAuthEffectState != EffectRefreshClaimed || stored.RefreshOwnerID != "active-process" ||
		stored.RefreshLeaseExpiresAt == nil || !stored.RefreshLeaseExpiresAt.Equal(leaseExpiresAt) || stored.RefreshDispatchStartedAt != nil {
		t.Fatalf("active claim was mutated: %+v", stored)
	}
}

func TestClaimedRefreshWithDispatchEvidenceFailsClosedAcrossEntries(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	dispatchedAt := time.Now().UTC().Add(-time.Minute)
	leaseExpiredAt := time.Now().UTC().Add(-time.Second)
	token.OAuthEffectState = EffectRefreshClaimed
	token.OAuthEffectVersion++
	token.RefreshOwnershipEpoch++
	token.RefreshOwnerID = "dead-process"
	token.RefreshLeaseExpiresAt = &leaseExpiredAt
	token.RefreshDispatchStartedAt = &dispatchedAt
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	service := newTestService(repository, provider)
	if _, err := service.ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrEffectStateInvalid) {
		t.Fatalf("ResolveAccessToken error = %v", err)
	}
	if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrEffectStateInvalid) {
		t.Fatalf("Refresh error = %v", err)
	}
	if provider.refreshCalls != 0 {
		t.Fatalf("refresh calls = %d", provider.refreshCalls)
	}
	repository.mu.Lock()
	defer repository.mu.Unlock()
	stored := repository.tokens[principal.ID]
	if stored.OAuthEffectState != EffectRefreshClaimed || stored.RefreshOwnerID != "dead-process" ||
		stored.RefreshDispatchStartedAt == nil || !stored.RefreshDispatchStartedAt.Equal(dispatchedAt) {
		t.Fatalf("invalid claim was mutated: %+v", stored)
	}
}

func TestUnknownTokenEffectStateFailsClosedWithoutProviderCall(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	repository.mu.Lock()
	token := repository.tokens[principal.ID]
	token.OAuthEffectState = "future_effect_state"
	repository.tokens[principal.ID] = token
	repository.mu.Unlock()

	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	if _, err := newTestService(repository, provider).Refresh(context.Background(), request); !errors.Is(err, ErrEffectStateInvalid) {
		t.Fatalf("Refresh error = %v", err)
	}
	authorizationCalls := len(provider.authorizations)
	if _, err := newTestService(repository, provider).BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrDisconnectRequired) {
		t.Fatalf("BeginAuthorization error = %v", err)
	}
	if provider.refreshCalls != 0 || len(provider.authorizations) != authorizationCalls {
		t.Fatalf("provider calls = refresh:%d authorization:%d/%d", provider.refreshCalls, len(provider.authorizations), authorizationCalls)
	}
}

func TestDisconnectRetainsTokenUntilDurableRevokeSuccess(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	if err := newTestService(repository, provider).Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); err != nil {
		t.Fatalf("Disconnect: %v", err)
	}
	if provider.revokeCalls != 1 {
		t.Fatalf("revoke calls = %d", provider.revokeCalls)
	}
	repository.mu.Lock()
	_, tokenExists := repository.tokens[principal.ID]
	stored := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
	repository.mu.Unlock()
	if tokenExists || stored.State != PrincipalDisconnected || stored.Generation != principal.Generation+1 {
		t.Fatalf("disconnect state = %+v tokenExists=%v", stored, tokenExists)
	}
}

func TestRefreshProviderFailuresRequireManualCleanupWithoutRevoke(t *testing.T) {
	for _, kind := range []ProviderErrorKind{ProviderErrorRejected, ProviderErrorTransient} {
		t.Run(string(kind), func(t *testing.T) {
			repository := newMemoryRepository()
			provider := testProvider()
			principal := connectPrincipal(t, repository, provider)
			provider.refreshErr = &ProviderError{Operation: "refresh", Kind: kind, Code: "provider_failure"}
			request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
				ExpectedGeneration: principal.Generation}
			service := newTestService(repository, provider)
			if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrManualActionRequired) {
				t.Fatalf("Refresh error = %v", err)
			}
			status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
			if err != nil || status.PrincipalState != PrincipalReconnectRequired ||
				status.EffectState != EffectManualRequired || !status.CanAttest {
				t.Fatalf("Status = %+v, %v", status, err)
			}
			provider.refreshErr = nil
			if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrManualActionRequired) {
				t.Fatalf("Disconnect error = %v", err)
			}
			if provider.refreshCalls != 1 || provider.revokeCalls != 0 {
				t.Fatalf("provider calls = refresh:%d revoke:%d", provider.refreshCalls, provider.revokeCalls)
			}
		})
	}
}

func TestCorruptStagedSecretsBecomeAttestableWithoutRepeatedProviderCalls(t *testing.T) {
	t.Run("authorization result", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		service := newTestService(repository, provider)
		if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
			t.Fatal(err)
		}
		state := provider.authorizations[0].State
		if _, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{UserID: "user-1", State: state, Code: "code"}); err != nil {
			t.Fatal(err)
		}
		repository.mu.Lock()
		record := repository.authorizations[digestString(state)]
		record.PendingTokenCiphertext = "corrupt"
		repository.authorizations[digestString(state)] = record
		repository.mu.Unlock()
		if _, err := service.BeginAuthorization(context.Background(), beginRequest()); !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("BeginAuthorization error = %v", err)
		}
		status, err := service.Status(context.Background(), "user-1", "provider-a")
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest || provider.identityCalls != 0 {
			t.Fatalf("Status/calls = %+v, %v, identity:%d", status, err, provider.identityCalls)
		}
	})

	t.Run("refresh result", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
			ExpectedGeneration: principal.Generation}
		service := newTestService(repository, provider)
		if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrEffectResultStaged) {
			t.Fatalf("Refresh error = %v", err)
		}
		repository.mu.Lock()
		token := repository.tokens[principal.ID]
		token.PendingTokenCiphertext = "corrupt"
		repository.tokens[principal.ID] = token
		repository.mu.Unlock()
		if _, err := service.ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("ResolveAccessToken error = %v", err)
		}
		status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest || provider.refreshCalls != 1 {
			t.Fatalf("Status/calls = %+v, %v, refresh:%d", status, err, provider.refreshCalls)
		}
	})

	t.Run("revoke token", func(t *testing.T) {
		repository := newMemoryRepository()
		provider := testProvider()
		principal := connectPrincipal(t, repository, provider)
		repository.mu.Lock()
		token := repository.tokens[principal.ID]
		token.RefreshTokenCiphertext = "corrupt"
		repository.tokens[principal.ID] = token
		repository.mu.Unlock()
		service := newTestService(repository, provider)
		if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrManualActionRequired) {
			t.Fatalf("Disconnect error = %v", err)
		}
		status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
		if err != nil || status.EffectState != EffectManualRequired || !status.CanAttest || provider.revokeCalls != 0 {
			t.Fatalf("Status/calls = %+v, %v, revoke:%d", status, err, provider.revokeCalls)
		}
	})
}

func TestUnavailableKeyLeavesStagedRefreshRetryable(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	service := newTestService(repository, provider)
	if _, err := service.Refresh(context.Background(), request); !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("Refresh error = %v", err)
	}
	service.cipher = purposeFailCipher{SecretCipher: service.cipher, openPurpose: SecretRefreshResult,
		err: ErrEncryptionKeyMissing}
	if _, err := service.ResolveAccessToken(context.Background(), request); !errors.Is(err, ErrEncryptionKeyMissing) {
		t.Fatalf("ResolveAccessToken error = %v", err)
	}
	status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if err != nil || status.EffectState != EffectRefreshResultStaged || status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, err)
	}
}

func TestRevokeFailureRetainsTokenAndRequiresManualActionOnRetry(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	provider.revokeErr = &ProviderError{Operation: "revoke", Kind: ProviderErrorUncertain, Code: "timeout"}
	service := newTestService(repository, provider)
	if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); err == nil {
		t.Fatal("Disconnect unexpectedly succeeded")
	}
	repository.mu.Lock()
	token, exists := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if !exists || token.OAuthEffectState != EffectManualRequired || token.RefreshOwnerID != "" || token.RefreshLeaseExpiresAt != nil {
		t.Fatalf("retained token = %+v exists=%v", token, exists)
	}
	if err := newTestService(repository, provider).Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrManualActionRequired) {
		t.Fatalf("retry error = %v", err)
	}
	if provider.revokeCalls != 1 {
		t.Fatalf("retry repeated revoke: %d", provider.revokeCalls)
	}
}

func TestExternalCleanupAttestationIsAuditedAndMakesNoProviderCall(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	provider.revokeErr = &ProviderError{Operation: "revoke", Kind: ProviderErrorUncertain, Code: "timeout"}
	service := newTestService(repository, provider)
	if err := service.Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); err == nil {
		t.Fatal("Disconnect unexpectedly succeeded")
	}
	status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
	if err != nil || !status.CanAttest || status.EffectState != EffectManualRequired {
		t.Fatalf("Status = %+v, %v", status, err)
	}

	audit := &fakeCleanupAudit{err: errors.New("audit unavailable")}
	service.audit = audit
	request := CleanupAttestation{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		Reason: "confirmed removal in Google account security settings"}
	if err := service.AttestExternalCleanup(context.Background(), request); err == nil {
		t.Fatal("attestation unexpectedly survived audit failure")
	}
	repository.mu.Lock()
	_, retained := repository.tokens[principal.ID]
	repository.mu.Unlock()
	if !retained {
		t.Fatal("audit failure did not roll back cleanup")
	}

	audit.err = nil
	if err := service.AttestExternalCleanup(context.Background(), request); err != nil {
		t.Fatalf("AttestExternalCleanup: %v", err)
	}
	repository.mu.Lock()
	_, retained = repository.tokens[principal.ID]
	repository.mu.Unlock()
	if retained || len(audit.events) != 1 || audit.events[0].EventID == "" ||
		audit.events[0].EffectState != EffectCleanupAttested || audit.events[0].PreviousEffectState != EffectManualRequired {
		t.Fatalf("cleanup/audit = retained:%v events:%+v", retained, audit.events)
	}
	if provider.revokeCalls != 1 {
		t.Fatalf("attestation made provider call: %d", provider.revokeCalls)
	}
}

func TestRevokeDispatchedRequiresLeaseExpiryAndExplicitAttestation(t *testing.T) {
	for _, expired := range []bool{false, true} {
		name := "active"
		if expired {
			name = "expired"
		}
		t.Run(name, func(t *testing.T) {
			repository := newMemoryRepository()
			provider := testProvider()
			principal := connectPrincipal(t, repository, provider)
			repository.mu.Lock()
			storedPrincipal := repository.principals[accountKey(principal.UserID, principal.ConnectorKey)]
			storedPrincipal.State = PrincipalDisconnected
			repository.principals[accountKey(principal.UserID, principal.ConnectorKey)] = storedPrincipal
			token := repository.tokens[principal.ID]
			now := time.Now().UTC()
			lease := now.Add(time.Minute)
			if expired {
				lease = now.Add(-time.Second)
			}
			token.OAuthEffectState = EffectRevokeDispatched
			token.OAuthEffectVersion++
			token.RefreshOwnershipEpoch++
			token.RefreshOwnerID = "effect-owner"
			token.RefreshLeaseExpiresAt = &lease
			token.RefreshDispatchStartedAt = &now
			repository.tokens[principal.ID] = token
			repository.mu.Unlock()

			service := newTestService(repository, provider)
			audit := &fakeCleanupAudit{}
			service.audit = audit
			status, err := service.Status(context.Background(), principal.UserID, principal.ConnectorKey)
			if err != nil || status.EffectState != EffectRevokeDispatched || status.CanAttest != expired {
				t.Fatalf("Status = %+v, %v", status, err)
			}
			if err := service.Revoke(context.Background(), principal.UserID, principal.ConnectorKey); !errors.Is(err, ErrRefreshInProgress) {
				t.Fatalf("Revoke error = %v", err)
			}
			attestErr := service.AttestExternalCleanup(context.Background(), CleanupAttestation{
				UserID: principal.UserID, ConnectorKey: principal.ConnectorKey, Reason: "cleanup confirmed",
			})
			if expired && attestErr != nil {
				t.Fatalf("expired AttestExternalCleanup error = %v", attestErr)
			}
			if !expired && !errors.Is(attestErr, ErrRefreshInProgress) {
				t.Fatalf("active AttestExternalCleanup error = %v", attestErr)
			}
			repository.mu.Lock()
			retainedToken, retained := repository.tokens[principal.ID]
			repository.mu.Unlock()
			if expired && (retained || len(audit.events) != 1 ||
				audit.events[0].PreviousEffectState != EffectRevokeDispatched || provider.revokeCalls != 0) {
				t.Fatalf("expired retained/audit/revoke = %v/%+v/%d", retained, audit.events, provider.revokeCalls)
			}
			if !expired && (!retained || retainedToken.OAuthEffectState != EffectRevokeDispatched ||
				retainedToken.RefreshOwnerID != "effect-owner" || retainedToken.RefreshOwnershipEpoch != token.RefreshOwnershipEpoch ||
				retainedToken.RefreshDispatchStartedAt == nil || !retainedToken.RefreshDispatchStartedAt.Equal(now) ||
				len(audit.events) != 0 || provider.revokeCalls != 0) {
				t.Fatalf("retained/audit/revoke = %v/%d/%d", retained, len(audit.events), provider.revokeCalls)
			}
		})
	}
}

func TestActiveExchangeCannotBeAttested(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	provider.exchangeStarted = make(chan struct{}, 1)
	provider.exchangeRelease = make(chan struct{})
	service := newTestService(repository, provider)
	service.audit = &fakeCleanupAudit{}
	if _, err := service.BeginAuthorization(context.Background(), beginRequest()); err != nil {
		t.Fatal(err)
	}
	state := provider.authorizations[0].State
	done := make(chan error, 1)
	go func() {
		_, err := service.CompleteAuthorization(context.Background(), CompleteAuthorizationRequest{
			UserID: "user-1", State: state, Code: "code",
		})
		done <- err
	}()
	<-provider.exchangeStarted

	status, err := service.Status(context.Background(), "user-1", "provider-a")
	if err != nil || status.EffectState != EffectExchangeDispatched || status.CanAttest {
		t.Fatalf("Status = %+v, %v", status, err)
	}
	request := CleanupAttestation{UserID: "user-1", ConnectorKey: "provider-a", Reason: "cleanup confirmed"}
	if err := service.AttestExternalCleanup(context.Background(), request); !errors.Is(err, ErrAuthorizationProgress) {
		t.Fatalf("active attestation error = %v", err)
	}
	repository.mu.Lock()
	retained := len(repository.authorizations) == 1
	repository.mu.Unlock()
	if !retained {
		t.Fatal("active attestation deleted authorization")
	}
	close(provider.exchangeRelease)
	if err := <-done; err != nil {
		t.Fatalf("CompleteAuthorization error = %v", err)
	}
}

func TestDisconnectFinalizesStagedRefreshBeforeRevoking(t *testing.T) {
	repository := newMemoryRepository()
	provider := testProvider()
	principal := connectPrincipal(t, repository, provider)
	request := ResolveAccessTokenRequest{UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		ExpectedGeneration: principal.Generation}
	if _, err := newTestService(repository, provider).Refresh(context.Background(), request); !errors.Is(err, ErrEffectResultStaged) {
		t.Fatalf("Refresh stage error = %v", err)
	}
	if err := newTestService(repository, provider).Disconnect(context.Background(), principal.UserID, principal.ConnectorKey); err != nil {
		t.Fatalf("Disconnect: %v", err)
	}
	if provider.refreshCalls != 1 || provider.revokeCalls != 1 || len(provider.revokedTokens) != 1 || provider.revokedTokens[0] != "refresh-rotated" {
		t.Fatalf("provider calls/tokens = refresh:%d revoke:%d tokens:%v", provider.refreshCalls, provider.revokeCalls, provider.revokedTokens)
	}
}
