package oidcsession

import (
	"bytes"
	"context"
	"errors"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

func TestServiceStoresOnlyHashedSessionAndEncryptedTokens(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"access-token": {ID: "subject-1", Source: "oidc", Roles: []string{"user"}, TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	service := newTestService(t, repository, verifier, &fakePublicClient{})
	service.now = func() time.Time { return now }
	localUser := &auth.User{ID: "subject-1", GatewayUserID: "11111111-1111-4111-8111-111111111111", GatewayTenantID: "22222222-2222-4222-8222-222222222222"}
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access-token", RefreshToken: "refresh-token", IDToken: "id-token"}, localUser)
	if err != nil {
		t.Fatal(err)
	}
	if len(raw) != 43 {
		t.Fatalf("opaque session length = %d, want 43", len(raw))
	}
	record := repository.snapshot()
	if bytes.Contains(record.TokenCiphertext, []byte("access-token")) || bytes.Contains(record.TokenCiphertext, []byte("refresh-token")) {
		t.Fatal("repository received plaintext token material")
	}
	hash, _ := sessionTokenHash(raw)
	if !bytes.Equal(hash, record.SessionTokenHash) || bytes.Equal([]byte(raw), record.SessionTokenHash) {
		t.Fatal("repository did not receive only the SHA-256 session hash")
	}
	user, err := service.Resolve(context.Background(), raw)
	if err != nil || user.ID != "subject-1" {
		t.Fatalf("resolve user=%#v err=%v", user, err)
	}
}

func TestServiceConcurrentExpiredRequestsRefreshExactlyOnce(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"old-access": {ID: "subject-1", Source: "oidc", Roles: []string{"user"}, TokenExpiresAt: now.Add(5 * time.Minute)},
		"new-access": {ID: "subject-1", Source: "oidc", Roles: []string{"user"}, TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	client := &fakePublicClient{refreshResponse: auth.OIDCTokenResponse{AccessToken: "new-access", RefreshToken: "rotated-refresh", ExpiresIn: 300}, delay: 40 * time.Millisecond}
	service := newTestService(t, repository, verifier, client)
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "old-access", RefreshToken: "old-refresh"}, &auth.User{
		ID: "subject-1", GatewayUserID: "11111111-1111-4111-8111-111111111111", GatewayTenantID: "22222222-2222-4222-8222-222222222222",
	})
	if err != nil {
		t.Fatal(err)
	}
	repository.expireAccessToken(now.Add(-time.Second))

	var wait sync.WaitGroup
	errorsFound := make(chan error, 20)
	for range 20 {
		wait.Add(1)
		go func() {
			defer wait.Done()
			user, resolveErr := service.Resolve(context.Background(), raw)
			if resolveErr != nil {
				errorsFound <- resolveErr
				return
			}
			if user.ID != "subject-1" {
				errorsFound <- errors.New("wrong resolved subject")
			}
		}()
	}
	wait.Wait()
	close(errorsFound)
	for err := range errorsFound {
		t.Errorf("concurrent resolve: %v", err)
	}
	if got := client.refreshCalls.Load(); got != 1 {
		t.Fatalf("refresh calls = %d, want exactly 1", got)
	}
	if repository.snapshot().RefreshVersion != 2 {
		t.Fatalf("refresh version = %d, want 2", repository.snapshot().RefreshVersion)
	}
}

func TestServiceDoesNotRefreshExpiredIdleSession(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{"access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)}}}
	client := &fakePublicClient{refreshResponse: auth.OIDCTokenResponse{AccessToken: "new", RefreshToken: "rotated"}}
	service := newTestService(t, repository, verifier, client)
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access", RefreshToken: "refresh"}, &auth.User{
		ID: "subject-1", GatewayUserID: "11111111-1111-4111-8111-111111111111", GatewayTenantID: "22222222-2222-4222-8222-222222222222",
	})
	if err != nil {
		t.Fatal(err)
	}
	repository.expireIdle(now.Add(-time.Second))
	if _, err := service.Resolve(context.Background(), raw); !errors.Is(err, ErrSessionExpired) {
		t.Fatalf("Resolve() error = %v, want ErrSessionExpired", err)
	}
	if client.refreshCalls.Load() != 0 {
		t.Fatal("expired idle session attempted a refresh")
	}
}

func TestServiceKeepsExistingRefreshTokenWhenIssuerDoesNotRotate(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"old-access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)},
		"new-access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	client := &fakePublicClient{refreshResponse: auth.OIDCTokenResponse{AccessToken: "new-access"}}
	service := newTestService(t, repository, verifier, client)
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "old-access", RefreshToken: "existing-refresh"}, testLocalUser())
	if err != nil {
		t.Fatal(err)
	}
	repository.expireAccessToken(now.Add(-time.Second))
	if _, err := service.Resolve(context.Background(), raw); err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	bundle, err := service.Delete(context.Background(), raw)
	if err != nil {
		t.Fatal(err)
	}
	if bundle.RefreshToken != "existing-refresh" || bundle.AccessToken != "new-access" {
		t.Fatal("refreshed bundle did not retain the existing refresh token")
	}
}

func TestServiceInvalidGrantDeletesSession(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	client := &fakePublicClient{refreshError: auth.ErrOIDCInvalidGrant}
	service := newTestService(t, repository, verifier, client)
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access", RefreshToken: "revoked-refresh"}, testLocalUser())
	if err != nil {
		t.Fatal(err)
	}
	repository.expireAccessToken(now.Add(-time.Second))
	if _, err := service.Resolve(context.Background(), raw); !errors.Is(err, ErrSessionExpired) {
		t.Fatalf("Resolve() error = %v, want ErrSessionExpired", err)
	}
	if !repository.isDeleted() || client.refreshCalls.Load() != 1 {
		t.Fatal("invalid_grant did not delete the session after exactly one refresh")
	}
}

func TestServiceUsesStillValidAccessTokenWhenRefreshIsTemporarilyUnavailable(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(30 * time.Second)},
	}}
	client := &fakePublicClient{refreshError: context.DeadlineExceeded}
	service := newTestService(t, repository, verifier, client)
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access", RefreshToken: "refresh"}, testLocalUser())
	if err != nil {
		t.Fatal(err)
	}
	user, err := service.Resolve(context.Background(), raw)
	if err != nil || user.ID != "subject-1" {
		t.Fatalf("Resolve() user=%#v error=%v", user, err)
	}
	if client.refreshCalls.Load() != 1 || repository.isDeleted() {
		t.Fatal("temporary refresh failure did not fall back to the valid access token")
	}
}

func TestServiceReturnsUnavailableWhenExpiredTokenCannotRefresh(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	service := newTestService(t, repository, verifier, &fakePublicClient{refreshError: context.DeadlineExceeded})
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access", RefreshToken: "refresh"}, testLocalUser())
	if err != nil {
		t.Fatal(err)
	}
	repository.expireAccessToken(now.Add(-time.Second))
	if _, err := service.Resolve(context.Background(), raw); !errors.Is(err, ErrSessionRefreshUnavailable) {
		t.Fatalf("Resolve() error = %v, want ErrSessionRefreshUnavailable", err)
	}
}

func TestServiceRejectsWrongEncryptionKeyAndDisabledUser(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	service := newTestService(t, repository, verifier, &fakePublicClient{})
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access", RefreshToken: "refresh"}, testLocalUser())
	if err != nil {
		t.Fatal(err)
	}
	wrongCipher, err := NewCipher(bytes.Repeat([]byte{8}, 32))
	if err != nil {
		t.Fatal(err)
	}
	wrongKeyService, err := NewService(repository, wrongCipher, verifier, &fakePublicClient{}, Config{
		IdleTTL: 30 * time.Minute, AbsoluteTTL: 8 * time.Hour, RefreshBefore: time.Minute,
	})
	if err != nil {
		t.Fatal(err)
	}
	wrongKeyService.now = func() time.Time { return now }
	if _, err := wrongKeyService.Resolve(context.Background(), raw); !errors.Is(err, ErrSessionStoreUnavailable) {
		t.Fatalf("wrong-key Resolve() error = %v, want ErrSessionStoreUnavailable", err)
	}

	repository.disableUser()
	if _, err := service.Resolve(context.Background(), raw); !errors.Is(err, ErrGatewayUserDisabled) {
		t.Fatalf("disabled-user Resolve() error = %v, want ErrGatewayUserDisabled", err)
	}
}

func TestServiceDeletesSessionEvenWhenCiphertextCannotBeDecryptedDuringLogout(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	repository := newFakeRepository("subject-1")
	verifier := fakeVerifier{users: map[string]*auth.User{
		"access": {ID: "subject-1", Source: "oidc", TokenExpiresAt: now.Add(5 * time.Minute)},
	}}
	service := newTestService(t, repository, verifier, &fakePublicClient{})
	service.now = func() time.Time { return now }
	raw, err := service.Create(context.Background(), TokenBundle{AccessToken: "access", RefreshToken: "refresh"}, testLocalUser())
	if err != nil {
		t.Fatal(err)
	}
	repository.corruptCiphertext()
	if _, err := service.Delete(context.Background(), raw); err != nil {
		t.Fatalf("Delete() error = %v", err)
	}
	if !repository.isDeleted() {
		t.Fatal("logout left a session with unusable ciphertext in the store")
	}
}

func TestServiceBindsMultiTenantSessionToIssuerApplicationTenantAndClient(t *testing.T) {
	now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
	tenantA := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
	tenantB := "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
	applicationID := "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
	verified := &auth.User{
		ID: "shared-subject", Source: "oidc", Issuer: "https://auth.example.test",
		ApplicationID: applicationID, TenantID: tenantA, OIDCClientID: "gateway-browser",
		TokenExpiresAt: now.Add(5 * time.Minute),
	}
	verifier := fakeVerifier{users: map[string]*auth.User{"access": verified}}
	repository := newFakeRepository("shared-subject")
	service := newTestService(t, repository, verifier, &fakePublicClient{})
	service.now = func() time.Time { return now }
	local := &auth.User{
		ID: "shared-subject", GatewayUserID: "11111111-1111-4111-8111-111111111111",
		GatewayTenantID:   "22222222-2222-4222-8222-222222222222",
		OIDCUserBindingID: "44444444-4444-4444-8444-444444444444",
		Issuer:            "https://auth.example.test", ApplicationID: applicationID,
		TenantID: tenantA, OIDCClientID: "gateway-browser",
	}
	raw, err := service.Create(context.Background(), TokenBundle{
		AccessToken: "access", RefreshToken: "refresh",
	}, local)
	if err != nil {
		t.Fatalf("Create() error = %v", err)
	}
	verified.TenantID = tenantB
	if _, err := service.Resolve(context.Background(), raw); !errors.Is(err, ErrSessionInvalid) {
		t.Fatalf("Resolve() error = %v, want ErrSessionInvalid", err)
	}
	if !repository.isDeleted() {
		t.Fatal("identity-binding mismatch did not destroy the session")
	}
}

func TestServiceBindsPlatformSessionToExplicitContextWithoutTenant(t *testing.T) {
	now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
	applicationID := "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
	verified := &auth.User{
		ID: "platform-subject", Source: "oidc",
		Issuer:        "https://auth.example.test",
		ApplicationID: applicationID, ContextType: "platform",
		OIDCClientID:   "gateway-browser",
		TokenExpiresAt: now.Add(5 * time.Minute),
	}
	repository := newFakeRepository("platform-subject")
	service := newTestService(
		t,
		repository,
		fakeVerifier{users: map[string]*auth.User{"access": verified}},
		&fakePublicClient{},
	)
	service.now = func() time.Time { return now }
	raw, err := service.Create(
		context.Background(),
		TokenBundle{AccessToken: "access", RefreshToken: "refresh"},
		&auth.User{
			ID:              "platform-subject",
			GatewayUserID:   "11111111-1111-4111-8111-111111111111",
			GatewayTenantID: "22222222-2222-4222-8222-222222222222",
			ContextType:     "platform",
		},
	)
	if err != nil {
		t.Fatalf("Create() error = %v", err)
	}
	record := repository.snapshot()
	if record.ContextType != "platform" || record.TenantID != "" ||
		record.Issuer != verified.Issuer ||
		record.ApplicationID != applicationID {
		t.Fatalf("platform session record=%#v", record)
	}
	verified.ContextType = "tenant"
	verified.TenantID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
	if _, err := service.Resolve(
		context.Background(),
		raw,
	); !errors.Is(err, ErrSessionInvalid) {
		t.Fatalf("Resolve() error = %v, want ErrSessionInvalid", err)
	}
}

func TestServiceCreateReportsSafeFailureCategory(t *testing.T) {
	now := time.Date(2026, 7, 29, 9, 0, 0, 0, time.UTC)
	validVerified := &auth.User{
		ID: "subject-1", Source: "oidc", Issuer: "http://localhost:18003",
		ApplicationID: "application-1", TenantID: "tenant-1", OIDCClientID: "gateway-browser",
		TokenExpiresAt: now.Add(5 * time.Minute),
	}
	validLocal := &auth.User{
		ID: "subject-1", GatewayUserID: "11111111-1111-4111-8111-111111111111",
		GatewayTenantID:   "22222222-2222-4222-8222-222222222222",
		OIDCUserBindingID: "44444444-4444-4444-8444-444444444444",
		Issuer:            validVerified.Issuer, ApplicationID: validVerified.ApplicationID,
		TenantID: validVerified.TenantID, OIDCClientID: validVerified.OIDCClientID,
	}
	validBundle := TokenBundle{AccessToken: "access", RefreshToken: "refresh", IDToken: "id"}

	for _, test := range []struct {
		name         string
		repository   *fakeRepository
		verifier     fakeVerifier
		bundle       TokenBundle
		localUser    *auth.User
		wantCategory string
		wantRoot     error
	}{
		{
			name: "prerequisites invalid", repository: newFakeRepository("subject-1"),
			verifier: fakeVerifier{users: map[string]*auth.User{"access": validVerified}},
			bundle:   validBundle, wantCategory: "SESSION_PREREQUISITES_INVALID", wantRoot: ErrSessionInvalid,
		},
		{
			name: "access token invalid", repository: newFakeRepository("subject-1"),
			verifier: fakeVerifier{users: map[string]*auth.User{}},
			bundle:   validBundle, localUser: validLocal,
			wantCategory: "SESSION_ACCESS_TOKEN_INVALID", wantRoot: ErrSessionInvalid,
		},
		{
			name: "identity mismatch", repository: newFakeRepository("subject-1"),
			verifier: fakeVerifier{users: map[string]*auth.User{"access": validVerified}},
			bundle:   validBundle, localUser: func() *auth.User {
				copy := *validLocal
				copy.OIDCClientID = "different-client"
				return &copy
			}(),
			wantCategory: "SESSION_IDENTITY_MISMATCH", wantRoot: ErrSessionInvalid,
		},
		{
			name: "token expired", repository: newFakeRepository("subject-1"),
			verifier: fakeVerifier{users: map[string]*auth.User{"access": func() *auth.User {
				copy := *validVerified
				copy.TokenExpiresAt = now
				return &copy
			}()}},
			bundle: validBundle, localUser: validLocal,
			wantCategory: "SESSION_TOKEN_EXPIRED", wantRoot: ErrSessionExpired,
		},
		{
			name: "repository failed", repository: &fakeRepository{
				external: "subject-1", createError: errors.New("sensitive database detail"),
			},
			verifier: fakeVerifier{users: map[string]*auth.User{"access": validVerified}},
			bundle:   validBundle, localUser: validLocal,
			wantCategory: "SESSION_REPOSITORY_FAILED", wantRoot: ErrSessionStoreUnavailable,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			service := newTestService(t, test.repository, test.verifier, &fakePublicClient{})
			service.now = func() time.Time { return now }

			_, err := service.Create(context.Background(), test.bundle, test.localUser)

			if !errors.Is(err, test.wantRoot) {
				t.Fatalf("Create() error = %v, want root %v", err, test.wantRoot)
			}
			if category := SessionCreationCategory(err); category != test.wantCategory {
				t.Fatalf("SessionCreationCategory() = %q, want %q", category, test.wantCategory)
			}
			if strings.Contains(err.Error(), "sensitive database detail") {
				t.Fatal("session creation error exposed repository detail")
			}
		})
	}
}

func testLocalUser() *auth.User {
	return &auth.User{
		ID: "subject-1", GatewayUserID: "11111111-1111-4111-8111-111111111111",
		GatewayTenantID: "22222222-2222-4222-8222-222222222222",
	}
}

func newTestService(t *testing.T, repository Repository, verifier TokenVerifier, client PublicClient) *Service {
	t.Helper()
	cipher, err := NewCipher(bytes.Repeat([]byte{7}, 32))
	if err != nil {
		t.Fatal(err)
	}
	service, err := NewService(repository, cipher, verifier, client, Config{
		IdleTTL: 30 * time.Minute, AbsoluteTTL: 8 * time.Hour, RefreshBefore: time.Minute,
		RefreshLease: 5 * time.Second, RefreshWait: 2 * time.Second,
	})
	if err != nil {
		t.Fatal(err)
	}
	return service
}

type fakeVerifier struct{ users map[string]*auth.User }

func (f fakeVerifier) Verify(_ context.Context, token string) (*auth.User, error) {
	user := f.users[token]
	if user == nil {
		return nil, auth.ErrUnauthorized
	}
	copy := *user
	return &copy, nil
}

type fakePublicClient struct {
	refreshResponse auth.OIDCTokenResponse
	refreshError    error
	delay           time.Duration
	refreshCalls    atomic.Int64
}

func (f *fakePublicClient) Refresh(_ context.Context, _ string) (auth.OIDCTokenResponse, error) {
	f.refreshCalls.Add(1)
	if f.delay > 0 {
		time.Sleep(f.delay)
	}
	return f.refreshResponse, f.refreshError
}
func (f *fakePublicClient) RevokeRefreshToken(context.Context, string) error { return nil }
func (f *fakePublicClient) EndSessionURL(context.Context, string) (string, error) {
	return "https://gateway.example.com/", nil
}

type fakeRepository struct {
	mu          sync.Mutex
	record      store.OIDCSession
	deleted     bool
	external    string
	createError error
}

func newFakeRepository(external string) *fakeRepository { return &fakeRepository{external: external} }

func (f *fakeRepository) CreateOIDCSession(_ context.Context, input store.CreateOIDCSessionInput) (store.OIDCSession, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.createError != nil {
		return store.OIDCSession{}, f.createError
	}
	external := input.Subject
	if external == "" {
		external = f.external
	}
	f.record = store.OIDCSession{
		ID: "33333333-3333-4333-8333-333333333333", SessionTokenHash: append([]byte(nil), input.SessionTokenHash...),
		GatewayUserID: input.GatewayUserID, GatewayTenantID: input.GatewayTenantID, ExternalUserID: external,
		OIDCUserBindingID: input.OIDCUserBindingID, OIDCClientID: input.OIDCClientID,
		Issuer: input.Issuer, ApplicationID: input.ApplicationID,
		ContextType: input.ContextType, TenantID: input.TenantID,
		UserStatus: "active", TokenCiphertext: append([]byte(nil), input.TokenCiphertext...), AccessTokenExpiresAt: input.AccessTokenExpiresAt,
		LastSeenAt: input.LastSeenAt, IdleExpiresAt: input.IdleExpiresAt, AbsoluteExpiresAt: input.AbsoluteExpiresAt, RefreshVersion: 1,
	}
	return f.record, nil
}
func (f *fakeRepository) FindOIDCSessionByHash(_ context.Context, hash []byte) (store.OIDCSession, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.deleted || !bytes.Equal(hash, f.record.SessionTokenHash) {
		return store.OIDCSession{}, store.ErrOIDCSessionNotFound
	}
	item := f.record
	item.SessionTokenHash = append([]byte(nil), f.record.SessionTokenHash...)
	item.TokenCiphertext = append([]byte(nil), f.record.TokenCiphertext...)
	return item, nil
}
func (f *fakeRepository) TouchOIDCSession(_ context.Context, _ string, lastSeen, idle time.Time) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.deleted || !f.record.IdleExpiresAt.After(lastSeen) || !f.record.AbsoluteExpiresAt.After(lastSeen) {
		return store.ErrOIDCSessionNotFound
	}
	f.record.LastSeenAt, f.record.IdleExpiresAt = lastSeen, idle
	return nil
}
func (f *fakeRepository) AcquireOIDCSessionRefreshLock(_ context.Context, _ string, version int64, lockID string, until, now time.Time) (bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.deleted || f.record.RefreshVersion != version || f.record.RefreshLockID != "" && f.record.RefreshLockUntil.After(now) {
		return false, nil
	}
	f.record.RefreshLockID, f.record.RefreshLockUntil = lockID, until
	return true, nil
}
func (f *fakeRepository) CompleteOIDCSessionRefresh(_ context.Context, _ string, version int64, lockID string, ciphertext []byte, expires time.Time) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.deleted || f.record.RefreshVersion != version || f.record.RefreshLockID != lockID {
		return store.ErrOIDCSessionNotFound
	}
	f.record.TokenCiphertext = append([]byte(nil), ciphertext...)
	f.record.AccessTokenExpiresAt = expires
	f.record.RefreshVersion++
	f.record.RefreshLockID = ""
	f.record.RefreshLockUntil = time.Time{}
	return nil
}
func (f *fakeRepository) DeleteOIDCSessionByHash(context.Context, []byte) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.deleted = true
	return nil
}
func (f *fakeRepository) DeleteOIDCSessionByID(context.Context, string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.deleted = true
	return nil
}
func (f *fakeRepository) CleanupExpiredOIDCSessions(context.Context, time.Time) (int64, error) {
	return 0, nil
}
func (f *fakeRepository) snapshot() store.OIDCSession {
	f.mu.Lock()
	defer f.mu.Unlock()
	item := f.record
	item.TokenCiphertext = append([]byte(nil), item.TokenCiphertext...)
	return item
}
func (f *fakeRepository) expireAccessToken(expiry time.Time) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.record.AccessTokenExpiresAt = expiry
}
func (f *fakeRepository) expireIdle(expiry time.Time) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.record.IdleExpiresAt = expiry
}
func (f *fakeRepository) disableUser() {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.record.UserStatus = "disabled"
}
func (f *fakeRepository) corruptCiphertext() {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.record.TokenCiphertext = []byte("corrupt")
}
func (f *fakeRepository) isDeleted() bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.deleted
}
