// Copyright (C) MongoDB, Inc. 2022-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package oidcauth

import (
	"context"
	"fmt"
	"os"
	"path"
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"
	"unsafe"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/event"
	"go.mongodb.org/mongo-driver/v2/internal/require"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/auth"
)

var (
	uriAdmin     = os.Getenv("MONGODB_URI")
	uriSingle    = os.Getenv("MONGODB_URI_SINGLE")
	uriMulti     = os.Getenv("MONGODB_URI_MULTI")
	oidcTokenDir = os.Getenv("OIDC_TOKEN_DIR")
)

var oidcDomain = os.Getenv("OIDC_DOMAIN")

func explicitUser(user string) string {
	return fmt.Sprintf("%s@%s", user, oidcDomain)
}

func tokenFile(user string) string {
	return path.Join(oidcTokenDir, user)
}

func connectAdminClient() (*mongo.Client, error) {
	return mongo.Connect(options.Client().ApplyURI(uriAdmin))
}

func connectWithMachineCB(uri string, cb options.OIDCCallback) (*mongo.Client, error) {
	cred := options.Credential{
		AuthMechanism:       "MONGODB-OIDC",
		OIDCMachineCallback: cb,
	}
	optsBuilder := options.Client().ApplyURI(uri).SetAuth(cred)
	return mongo.Connect(optsBuilder)
}

func connectWithHumanCB(uri string, cb options.OIDCCallback) (*mongo.Client, error) {
	cred := options.Credential{
		AuthMechanism:     "MONGODB-OIDC",
		OIDCHumanCallback: cb,
	}
	opts := options.Client().ApplyURI(uri).SetAuth(cred)
	return mongo.Connect(opts)
}

func connectWithHumanCBAndUser(uri string, principal string, cb options.OIDCCallback) (*mongo.Client, error) {
	username := principal
	switch principal {
	case "test_user1", "test_user2":
		username = explicitUser(principal)
	}
	cred := options.Credential{
		AuthMechanism:       "MONGODB-OIDC",
		OIDCMachineCallback: cb,
		Username:            username,
	}
	opts := options.Client().ApplyURI(uri).SetAuth(cred)
	return mongo.Connect(opts)
}

func connectWithHumanCBAndMonitor(uri string, cb options.OIDCCallback, m *event.CommandMonitor) (*mongo.Client, error) {
	cred := options.Credential{
		AuthMechanism:     "MONGODB-OIDC",
		OIDCHumanCallback: cb,
	}
	opts := options.Client().ApplyURI(uri).SetMonitor(m).SetAuth(cred)
	return mongo.Connect(opts)
}

func connectWithMachineCBAndProperties(uri string, cb options.OIDCCallback, props map[string]string) (*mongo.Client, error) {
	cred := options.Credential{
		AuthMechanism:           "MONGODB-OIDC",
		OIDCMachineCallback:     cb,
		AuthMechanismProperties: props,
	}
	optsBuilder := options.Client().ApplyURI(uri).SetAuth(cred)
	return mongo.Connect(optsBuilder)
}

// isCallbackTest returns true if running with a callback (not built-in integration)
func isCallbackTest() bool {
	env := os.Getenv("OIDC_ENV")
	// Empty OIDC_ENV or "test" uses callbacks
	return env == "" || env == "test"
}

// connectWithOIDC creates an OIDC client appropriate for the current environment
// When using built-in integrations (azure, gcp, k8s), no callback is used
// When using test environment, uses the provided callback
func connectWithOIDC(uri string, cb options.OIDCCallback) (*mongo.Client, error) {
	if isCallbackTest() {
		return connectWithMachineCB(uri, cb)
	}
	return mongo.Connect(options.Client().ApplyURI(uri))
}

func TestMain(m *testing.M) {
	// Skip all tests if OIDC environment is not configured.
	// These tests require specific OIDC infrastructure and environment variables.
	// This skip is added to prevent the tests from running on go test ./... evergreen tasks
	if os.Getenv("MONGODB_URI_SINGLE") == "" {
		fmt.Println("Skipping OIDC tests: MONGODB_URI_SINGLE not set")
		os.Exit(0)
	}
	os.Exit(m.Run())
}

func TestMachine_1_1_CallbackIsCalled(t *testing.T) {
	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithOIDC(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	if isCallbackTest() {
		require.Equal(t, 1, callbackCount, "expected callback count to be 1")
		require.NoError(t, callbackFailed, "callback failed")
	}
}

func TestMachine_1_2_CallbackIsCalledOnlyOnce(t *testing.T) {
	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithOIDC(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	var wg sync.WaitGroup

	var findFailed error
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			coll := client.Database("test").Collection("test")
			_, err := coll.Find(context.Background(), bson.D{})
			if err != nil {
				findFailed = fmt.Errorf("failed executing Find: %v", err)
			}
		}()
	}

	wg.Wait()
	countMutex.Lock()
	defer countMutex.Unlock()
	if isCallbackTest() {
		require.Equal(t, 1, callbackCount, "expected callback count to be 1")
		require.NoError(t, callbackFailed, "callback failed")
	}
	require.NoError(t, findFailed, "find failed")
}

func TestMachine_2_1_ValidCallbackInputs(t *testing.T) {
	if !isCallbackTest() {
		t.Skip("Skipping: [callback-only] test")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithMachineCB(uriSingle, func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) {
		if args.RefreshToken != nil {
			callbackFailed = fmt.Errorf("expected RefreshToken to be nil, got %v", args.RefreshToken)
		}
		timeout, ok := ctx.Deadline()
		if !ok {
			callbackFailed = fmt.Errorf("expected context to have deadline, got %v", ctx)
		}
		if timeout.Before(time.Now()) {
			callbackFailed = fmt.Errorf("expected timeout to be in the future, got %v", timeout)
		}
		if args.Version < 1 {
			callbackFailed = fmt.Errorf("expected Version to be at least 1, got %d", args.Version)
		}
		if args.IDPInfo != nil {
			callbackFailed = fmt.Errorf("expected IdpID to be nil for Machine flow, got %v", args.IDPInfo)
		}
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			return nil, fmt.Errorf("failed reading token file: %w", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestMachine_2_3_OIDCCallbackReturnMissingData(t *testing.T) {
	if !isCallbackTest() {
		t.Skip("Skipping: [callback-only] test")
	}

	callbackCount := 0
	countMutex := sync.Mutex{}

	client, err := connectWithMachineCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		return &options.OIDCCredential{
			AccessToken:  "",
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "should have failed to execute Find, but succeeded")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
}

func TestMachine_2_4_InvalidClientConfigurationWithCallback(t *testing.T) {
	if !isCallbackTest() {
		t.Skip("Skipping: [callback-only] test")
	}

	_, err := connectWithMachineCBAndProperties(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		expiry := time.Now().Add(time.Hour)
		return &options.OIDCCredential{
			AccessToken:  "",
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	},
		map[string]string{"ENVIRONMENT": "test"},
	)
	require.Error(t, err, "should have failed to build client when it should fail")
}

func TestMachine_2_5_InvalidUseOfAllowedHosts(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "azure" {
		t.Skip("Skipping: test only runs when OIDC_ENV=azure")
	}

	_, err := connectWithMachineCBAndProperties(uriSingle, func(_ context.Context, _ *options.OIDCArgs) (*options.OIDCCredential, error) {
		expiry := time.Now().Add(time.Hour)
		return &options.OIDCCredential{
			AccessToken:  "",
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	},
		map[string]string{
			"ENVIRONMENT":   "azure",
			"ALLOWED_HOSTS": "",
		},
	)
	require.Error(t, err, "should have failed to build client when it should fail")
}

func TestMachine_3_1_FailureWithCachedTokens(t *testing.T) {
	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithOIDC(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	// Poison the cache with a random token
	clientElem := reflect.ValueOf(client).Elem()
	authenticatorField := clientElem.FieldByName("authenticator")
	authenticatorField = reflect.NewAt(
		authenticatorField.Type(),
		unsafe.Pointer(authenticatorField.UnsafeAddr())).Elem()
	// this is the only usage of the x packages in the test, showing the public interface is
	// correct.
	authenticatorField.Interface().(*auth.OIDCAuthenticator).SetAccessToken("some random happy sunshine string")

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	if isCallbackTest() {
		require.Equal(t, 1, callbackCount, "expected callback count to be 1")
		require.NoError(t, callbackFailed, "callback failed")
	}
}

func TestMachine_3_2_AuthFailuresWithoutCachedTokens(t *testing.T) {
	if !isCallbackTest() {
		t.Skip("Skipping: [callback-only] test")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithMachineCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		return &options.OIDCCredential{
			AccessToken:  "this is a bad, bad token",
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")
	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "should have failed to execute Find, but succeeded")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestMachine_3_3_UnexpectedErrorCodeDoesNotClearCache(t *testing.T) {
	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithOIDC(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"saslStart",
			}},
			{Key: "errorCode", Value: 20},
		}},
	})

	require.NoError(t, res.Err(), "failed setting failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "should have failed to execute Find, but succeeded")

	countMutex.Lock()
	defer countMutex.Unlock()
	if isCallbackTest() {
		require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	}

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	if isCallbackTest() {
		require.Equal(t, 1, callbackCount, "expected callback count to be 1")
		require.NoError(t, callbackFailed, "callback failed")
	}
}

func TestMachine_4_1_ReauthenticationSucceeds(t *testing.T) {
	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithOIDC(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")
	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed setting failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	if isCallbackTest() {
		require.Equal(t, 2, callbackCount, "expected callback count to be 2")
		require.NoError(t, callbackFailed, "callback failed")
	}
}

func TestMachine_4_2_ReadCommandsFailIfReauthenticationFails(t *testing.T) {
	if !isCallbackTest() {
		t.Skip("Skipping: [callback-only] test")
	}

	callbackCount := 0
	var callbackFailed error
	firstCall := true
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithMachineCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		if firstCall {
			firstCall = false
			tokenFile := tokenFile("test_user1")
			accessToken, err := os.ReadFile(tokenFile)
			if err != nil {
				callbackFailed = fmt.Errorf("failed reading token file: %v", err)
			}
			return &options.OIDCCredential{
				AccessToken:  string(accessToken),
				ExpiresAt:    &expiry,
				RefreshToken: nil,
			}, nil
		}
		return &options.OIDCCredential{
			AccessToken:  "this is a bad, bad token",
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")
	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed setting failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "should have failed to execute Find, but succeeded")

	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 2, callbackCount, "expected callback count to be 2")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestMachine_4_3_WriteCommandsFailIfReauthenticationFails(t *testing.T) {
	if !isCallbackTest() {
		t.Skip("Skipping: [callback-only] test")
	}

	callbackCount := 0
	var callbackFailed error
	firstCall := true
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithMachineCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		if firstCall {
			firstCall = false
			tokenFile := tokenFile("test_user1")
			accessToken, err := os.ReadFile(tokenFile)
			if err != nil {
				callbackFailed = fmt.Errorf("failed reading token file: %v", err)
			}
			return &options.OIDCCredential{
				AccessToken:  string(accessToken),
				ExpiresAt:    &expiry,
				RefreshToken: nil,
			}, nil
		}
		return &options.OIDCCredential{
			AccessToken:  "this is a bad, bad token",
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")
	_, err = coll.InsertOne(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Insert")

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"insert",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed setting failpoint")

	_, err = coll.InsertOne(context.Background(), bson.D{})
	require.Error(t, err, "should have failed to execute Insert, but succeeded")

	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 2, callbackCount, "expected callback count to be 2")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_1_SinglePrincipalImplicitUsername(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_2_SinglePrincipalExplicitUsername(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithHumanCBAndUser(uriSingle, "test_user1", func(_ context.Context, _ *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_3_MultiplePrincipalUser1(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}
	cb := func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	}
	cred := options.Credential{
		AuthMechanism:     "MONGODB-OIDC",
		OIDCHumanCallback: cb,
		Username:          explicitUser("test_user1"),
	}
	opts := options.Client().ApplyURI(uriMulti).SetAuth(cred)
	client, err := mongo.Connect(opts)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_4_MultiplePrincipalUser2(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}
	cb := func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user2")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	}
	cred := options.Credential{
		AuthMechanism:     "MONGODB-OIDC",
		OIDCHumanCallback: cb,
		Username:          explicitUser("test_user2"),
	}
	opts := options.Client().ApplyURI(uriMulti).SetAuth(cred)
	client, err := mongo.Connect(opts)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_5_MultiplePrincipalNoUser(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithHumanCB(uriMulti, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "should have failed to execute Find, but succeeded")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 0, callbackCount, "expected callback count to be 0")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_6_AllowedHostsBlocked(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	var callbackFailed error

	{
		cb := func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
			expiry := time.Now().Add(time.Hour)
			tokenFile := tokenFile("test_user1")
			accessToken, err := os.ReadFile(tokenFile)
			if err != nil {
				callbackFailed = fmt.Errorf("failed reading token file: %v", err)
			}
			return &options.OIDCCredential{
				AccessToken:  string(accessToken),
				ExpiresAt:    &expiry,
				RefreshToken: nil,
			}, nil
		}
		cred := options.Credential{
			AuthMechanism:           "MONGODB-OIDC",
			OIDCHumanCallback:       cb,
			AuthMechanismProperties: map[string]string{"ALLOWED_HOSTS": ""},
		}
		opts := options.Client().ApplyURI(uriMulti).SetAuth(cred)
		client, err := mongo.Connect(opts)
		require.NoError(t, err, "failed connecting client")
		defer func() { _ = client.Disconnect(context.Background()) }()

		coll := client.Database("test").Collection("test")

		_, err = coll.Find(context.Background(), bson.D{})
		require.Error(t, err, "should have failed to execute Find, but succeeded")
	}

	{
		cb := func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
			expiry := time.Now().Add(time.Hour)
			tokenFile := tokenFile("test_user1")
			accessToken, err := os.ReadFile(tokenFile)
			if err != nil {
				callbackFailed = fmt.Errorf("failed reading token file: %v", err)
			}
			return &options.OIDCCredential{
				AccessToken:  string(accessToken),
				ExpiresAt:    &expiry,
				RefreshToken: nil,
			}, nil
		}
		cred := options.Credential{
			AuthMechanism:           "MONGODB-OIDC",
			OIDCHumanCallback:       cb,
			AuthMechanismProperties: map[string]string{"ALLOWED_HOSTS": "example.com"},
		}
		opts := options.Client().ApplyURI("mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com").SetAuth(cred)
		client, err := mongo.Connect(opts)
		require.NoError(t, err, "failed connecting client")
		defer func() { _ = client.Disconnect(context.Background()) }()

		coll := client.Database("test").Collection("test")

		_, err = coll.Find(context.Background(), bson.D{})
		require.Error(t, err, "should have failed to execute Find, but succeeded")
	}

	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_1_7_AllowedHostsInConnectionStringIgnored(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	uri := "mongodb+srv://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22example.com%22%5D"
	opts := options.Client().ApplyURI(uri)
	err := opts.Validate()
	require.Error(t, err, "should have failed to apply URI which should produce an error")
}

func TestHuman_1_8_MachineIDPHumanCallback(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	if _, ok := os.LookupEnv("OIDC_IS_LOCAL"); !ok {
		t.Skip("Skipping: test only runs when OIDC_IS_LOCAL is set")
	}
	callbackCount := 0

	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithHumanCBAndUser(uriSingle, "test_machine", func(_ context.Context, _ *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_machine")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_2_1_ValidCallbackInputs(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	client, err := connectWithHumanCB(uriSingle, func(_ context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		if args.Version != 1 {
			callbackFailed = fmt.Errorf("expected version to be 1, got %d", args.Version)
		}
		if args.IDPInfo == nil {
			callbackFailed = fmt.Errorf("expected IDPInfo to be non-nil, previous error: (%v)", callbackFailed)
		}
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v, previous error: (%v)", err, callbackFailed)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_2_2_CallbackReturnsMissingData(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	countMutex := sync.Mutex{}

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		return &options.OIDCCredential{}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "Find should have failed")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
}

func TestHuman_2_3_RefreshTokenIsPassedToCallback(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithHumanCB(uriSingle, func(_ context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		if callbackCount == 1 && args.RefreshToken != nil {
			callbackFailed = fmt.Errorf("expected refresh token to be nil first time, got %v, previous error: (%v)", args.RefreshToken, callbackFailed)
		}
		if callbackCount == 2 && args.RefreshToken == nil {
			callbackFailed = fmt.Errorf("expected refresh token to be non-nil second time, got %v, previous error: (%v)", args.RefreshToken, callbackFailed)
		}
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		rt := "this is fake"
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: &rt,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed to set failpoint")

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	defer countMutex.Unlock()
	require.Equal(t, 2, callbackCount, "expected callback count to be 2")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_3_1_UsesSpeculativeAuth(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		// the callback should not even be called due to spec auth.
		return &options.OIDCCredential{}, nil
	})

	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	// We deviate from the Prose test since the failPoint on find with no error code does not seem to
	// work. Rather we put an access token in the cache to force speculative auth.
	tokenFile := tokenFile("test_user1")
	accessToken, err := os.ReadFile(tokenFile)
	require.NoError(t, err, "failed reading token file")
	clientElem := reflect.ValueOf(client).Elem()
	authenticatorField := clientElem.FieldByName("authenticator")
	authenticatorField = reflect.NewAt(
		authenticatorField.Type(),
		unsafe.Pointer(authenticatorField.UnsafeAddr())).Elem()
	// This is the only usage of the x packages in the test, showing the public interface is
	// correct.
	authenticatorField.Interface().(*auth.OIDCAuthenticator).SetAccessToken(string(accessToken))

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"saslStart",
			}},
			{Key: "errorCode", Value: 18},
		}},
	})

	require.NoError(t, res.Err(), "failed to set failpoint")

	coll := client.Database("test").Collection("test")
	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
}

func TestHuman_3_2_DoesNotUseSpeculativeAuth(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	var callbackFailed error

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"saslStart",
			}},
			{Key: "errorCode", Value: 18},
		}},
	})

	require.NoError(t, res.Err(), "failed to set failpoint")

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "Find should have failed")
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_4_1_ReauthenticationSucceeds(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	clearChannels := func(s chan *event.CommandStartedEvent, succ chan *event.CommandSucceededEvent, f chan *event.CommandFailedEvent) {
		for len(s) > 0 {
			<-s
		}
		for len(succ) > 0 {
			<-succ
		}
		for len(f) > 0 {
			<-f
		}
	}

	started := make(chan *event.CommandStartedEvent, 100)
	succeeded := make(chan *event.CommandSucceededEvent, 100)
	failed := make(chan *event.CommandFailedEvent, 100)

	monitor := event.CommandMonitor{
		Started: func(_ context.Context, e *event.CommandStartedEvent) {
			started <- e
		},
		Succeeded: func(_ context.Context, e *event.CommandSucceededEvent) {
			succeeded <- e
		},
		Failed: func(_ context.Context, e *event.CommandFailedEvent) {
			failed <- e
		},
	}

	client, err := connectWithHumanCBAndMonitor(uriSingle, func(_ context.Context, _ *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	}, &monitor)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })
	clearChannels(started, succeeded, failed)

	coll := client.Database("test").Collection("test")
	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	countMutex.Unlock()
	clearChannels(started, succeeded, failed)

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed setting failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
	countMutex.Lock()
	require.Equal(t, 2, callbackCount, "expected callback count to be 2")
	countMutex.Unlock()

	require.Equal(t, 2, len(started), "expected 2 finds started")
	for len(started) > 0 {
		ste := <-started
		require.Equal(t, "find", ste.CommandName, "found unexpected command started")
	}
	require.Equal(t, 1, len(succeeded), "expected 1 find to succeed")
	for len(succeeded) > 0 {
		sue := <-succeeded
		require.Equal(t, "find", sue.CommandName, "found unexpected command succeeded")
	}
	require.Equal(t, 1, len(failed), "expected 1 find to fail")
	for len(failed) > 0 {
		fe := <-failed
		require.Equal(t, "find", fe.CommandName, "found unexpected command failed")
	}

	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_4_2_ReauthenticationSucceedsNoRefreshToken(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: nil,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")

	countMutex.Lock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	countMutex.Unlock()

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed to set failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")

	countMutex.Lock()
	require.Equal(t, 2, callbackCount, "expected callback count to be 2")
	countMutex.Unlock()
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_4_3_ReauthenticationSucceedsAfterRefreshFails(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		expiry := time.Now().Add(time.Hour)
		tokenFile := tokenFile("test_user1")
		accessToken, err := os.ReadFile(tokenFile)
		if err != nil {
			callbackFailed = fmt.Errorf("failed reading token file: %v", err)
		}
		refreshToken := "bad token"
		return &options.OIDCCredential{
			AccessToken:  string(accessToken),
			ExpiresAt:    &expiry,
			RefreshToken: &refreshToken,
		}, nil
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")

	countMutex.Lock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	countMutex.Unlock()

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed to set failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")

	countMutex.Lock()
	require.Equal(t, 2, callbackCount, "expected callback count to be 2")
	countMutex.Unlock()
	require.NoError(t, callbackFailed, "callback failed")
}

func TestHuman_4_4_ReauthenticationFails(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "" {
		t.Skip("Skipping: test only runs when OIDC_ENV is empty")
	}

	callbackCount := 0
	var callbackFailed error
	countMutex := sync.Mutex{}

	adminClient, err := connectAdminClient()
	require.NoError(t, err, "failed connecting admin client")
	t.Cleanup(func() { _ = adminClient.Disconnect(context.Background()) })

	client, err := connectWithHumanCB(uriSingle, func(context.Context, *options.OIDCArgs) (*options.OIDCCredential, error) {
		countMutex.Lock()
		defer countMutex.Unlock()
		callbackCount++
		badToken := "bad token"
		expiry := time.Now().Add(time.Hour)
		if callbackCount == 1 {
			tokenFile := tokenFile("test_user1")
			accessToken, err := os.ReadFile(tokenFile)
			if err != nil {
				callbackFailed = fmt.Errorf("failed reading token file: %v", err)
			}
			return &options.OIDCCredential{
				AccessToken:  string(accessToken),
				ExpiresAt:    &expiry,
				RefreshToken: &badToken,
			}, nil
		}
		return &options.OIDCCredential{
			AccessToken:  badToken,
			ExpiresAt:    &expiry,
			RefreshToken: &badToken,
		}, fmt.Errorf("failed to refresh token")
	})
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")

	countMutex.Lock()
	require.Equal(t, 1, callbackCount, "expected callback count to be 1")
	countMutex.Unlock()

	res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{
		{Key: "configureFailPoint", Value: "failCommand"},
		{Key: "mode", Value: bson.D{
			{Key: "times", Value: 1},
		}},
		{Key: "data", Value: bson.D{
			{Key: "failCommands", Value: bson.A{
				"find",
			}},
			{Key: "errorCode", Value: 391},
		}},
	})

	require.NoError(t, res.Err(), "failed to set failpoint")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "Find should have failed")

	countMutex.Lock()
	require.Equal(t, 3, callbackCount, "expected callback count to be 3")
	countMutex.Unlock()
	require.NoError(t, callbackFailed, "callback failed")
}

func TestMachine_5_1_AzureWithNoUsername(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "azure" {
		t.Skip("Skipping: test only runs when OIDC_ENV=azure")
	}

	opts := options.Client().ApplyURI(uriSingle)
	require.NotNil(t, opts, "failed parsing uri: %q", uriSingle)
	client, err := mongo.Connect(opts)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
}

func TestMachine_5_2_AzureWithBadUsername(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "azure" {
		t.Skip("Skipping: test only runs when OIDC_ENV=azure")
	}

	opts := options.Client().ApplyURI(uriSingle)

	require.NotNil(t, opts, "failed parsing uri: %q", uriSingle)
	require.False(
		t,
		opts.Auth == nil || opts.Auth.AuthMechanism != "MONGODB-OIDC",
		"expected URI to contain MONGODB-OIDC auth information",
	)

	opts.Auth.Username = "bad"

	client, err := mongo.Connect(opts)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.Error(t, err, "Find should have failed")
}

func TestMachine_6_1_GCPWithNoUsername(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "gcp" {
		t.Skip("Skipping: test only runs when OIDC_ENV=gcp")
	}

	opts := options.Client().ApplyURI(uriSingle)
	client, err := mongo.Connect(opts)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
}

// TestMachine_K8s tests the "k8s" Kubernetes OIDC environment. There is no specified
// prose test for "k8s", so this test simply checks that you can run a "find"
// with the provided conn string.
func TestMachine_K8s(t *testing.T) {
	if os.Getenv("OIDC_ENV") != "k8s" {
		t.Skip("Skipping: test only runs when OIDC_ENV=k8s")
	}

	require.True(t, strings.Contains(uriSingle, "ENVIRONMENT:k8s"), "expected MONGODB_URI_SINGLE to specify ENVIRONMENT:k8s for Kubernetes test")

	opts := options.Client().ApplyURI(uriSingle)
	client, err := mongo.Connect(opts)
	require.NoError(t, err, "failed connecting client")
	t.Cleanup(func() { _ = client.Disconnect(context.Background()) })

	coll := client.Database("test").Collection("test")

	_, err = coll.Find(context.Background(), bson.D{})
	require.NoError(t, err, "failed executing Find")
}
