// Copyright (C) MongoDB, Inc. 2017-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 unified

import (
	"context"
	"crypto/tls"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/event"
	"go.mongodb.org/mongo-driver/v2/internal/handshake"
	"go.mongodb.org/mongo-driver/v2/internal/integration/mtest"
	"go.mongodb.org/mongo-driver/v2/internal/integtest"
	"go.mongodb.org/mongo-driver/v2/internal/logger"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/mongo/readconcern"
	"go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/description"
)

// There are no automated tests for truncation. Given that, setting the
// "MaxDocumentLength" to 10_000 will ensure that the default truncation
// length does not interfere with tests with commands/replies that
// exceed the default truncation length.
const defaultMaxDocumentLen = 10_000

// Security-sensitive commands that should be ignored in command monitoring by default.
var securitySensitiveCommands = []string{
	"authenticate", "saslStart", "saslContinue", "getnonce",
	"createUser", "updateUser", "copydbgetnonce", "copydbsaslstart", "copydb",
}

// clientEntity is a wrapper for a mongo.Client object that also holds additional information required during test
// execution.
type clientEntity struct {
	*mongo.Client
	disconnected bool

	recordEvents                atomic.Value
	started                     []*event.CommandStartedEvent
	succeeded                   []*event.CommandSucceededEvent
	failed                      []*event.CommandFailedEvent
	pooled                      []*event.PoolEvent
	serverDescriptionChanged    []*event.ServerDescriptionChangedEvent
	serverHeartbeatFailedEvent  []*event.ServerHeartbeatFailedEvent
	serverHeartbeatStartedEvent []*event.ServerHeartbeatStartedEvent
	serverHeartbeatSucceeded    []*event.ServerHeartbeatSucceededEvent
	topologyDescriptionChanged  []*event.TopologyDescriptionChangedEvent
	topologyOpening             []*event.TopologyOpeningEvent
	topologyClosed              []*event.TopologyClosedEvent
	ignoredCommands             map[string]struct{}
	observeSensitiveCommands    *bool
	numConnsCheckedOut          int32
	latestDesc                  event.TopologyDescription
	connsPerServer              map[string]int

	// These should not be changed after the clientEntity is initialized
	observedEvents                      map[monitoringEventType]struct{}
	storedEvents                        map[monitoringEventType][]string
	eventsCount                         map[monitoringEventType]int32
	serverDescriptionChangedEventsCount map[serverDescriptionChangedEventInfo]int32

	eventsCountLock                         sync.RWMutex
	serverDescriptionChangedEventsCountLock sync.RWMutex
	eventProcessMu                          sync.RWMutex

	entityMap *EntityMap

	logQueue chan orderedLogMessage
}

func newClientEntity(ctx context.Context, em *EntityMap, entityOptions *entityOptions) (*clientEntity, error) {
	// The "configureFailPoint" command should always be ignored.
	ignoredCommands := map[string]struct{}{
		"configureFailPoint": {},
	}
	// If not observing sensitive commands, add security-sensitive commands
	// to ignoredCommands by default.
	if entityOptions.ObserveSensitiveCommands == nil || !*entityOptions.ObserveSensitiveCommands {
		for _, cmd := range securitySensitiveCommands {
			ignoredCommands[cmd] = struct{}{}
		}
	}
	entity := &clientEntity{
		ignoredCommands:                     ignoredCommands,
		observedEvents:                      make(map[monitoringEventType]struct{}),
		storedEvents:                        make(map[monitoringEventType][]string),
		eventsCount:                         make(map[monitoringEventType]int32),
		serverDescriptionChangedEventsCount: make(map[serverDescriptionChangedEventInfo]int32),
		entityMap:                           em,
		observeSensitiveCommands:            entityOptions.ObserveSensitiveCommands,
		connsPerServer:                      make(map[string]int),
	}
	entity.setRecordEvents(true)

	// Construct a ClientOptions instance by first applying the cluster URI and then the URIOptions map to ensure that
	// the options specified in the test file take precedence.
	uri := getURIForClient(entityOptions)
	clientOpts := options.Client().ApplyURI(uri)
	if entityOptions.URIOptions != nil {
		if err := setClientOptionsFromURIOptions(clientOpts, entityOptions.URIOptions); err != nil {
			return nil, fmt.Errorf("error parsing URI options: %w", err)
		}
	}

	if olm := entityOptions.ObserveLogMessages; olm != nil {
		expectedLogMessagesCount := expectedLogMessagesCount(ctx, entityOptions.ID)
		ignoreLogMessages := ignoreLogMessages(ctx, entityOptions.ID)

		clientLogger := newLogger(olm, expectedLogMessagesCount, ignoreLogMessages)

		wrap := func(str string) options.LogLevel {
			return options.LogLevel(logger.ParseLevel(str))
		}

		// Assign the log queue to the entity so that it can be used to
		// retrieve log messages.
		entity.logQueue = clientLogger.logQueue

		// Update the client options to add the clientLogger.
		loggerOptions := options.Logger().
			SetComponentLevel(options.LogComponentCommand, wrap(olm.Command)).
			SetComponentLevel(options.LogComponentTopology, wrap(olm.Topology)).
			SetComponentLevel(options.LogComponentServerSelection, wrap(olm.ServerSelection)).
			SetComponentLevel(options.LogComponentConnection, wrap(olm.Connection)).
			SetMaxDocumentLength(defaultMaxDocumentLen).
			SetSink(clientLogger)

		clientOpts.SetLoggerOptions(loggerOptions)
	}

	// UseMultipleMongoses requires validation when connecting to a sharded cluster. Options changes and validation are
	// only required if the option is explicitly set. If it's unset, we make no changes because the cluster URI already
	// includes all nodes and we don't enforce any limits on the number of nodes.
	if mtest.ClusterTopologyKind() == mtest.Sharded && entityOptions.UseMultipleMongoses != nil {
		if err := evaluateUseMultipleMongoses(clientOpts, *entityOptions.UseMultipleMongoses); err != nil {
			return nil, err
		}
	}

	if entityOptions.ObserveEvents != nil || entityOptions.StoreEventsAsEntities != nil {
		// Configure a command monitor that listens for the specified event types. We don't take the IgnoredCommands
		// option into account here because it can be overridden at the test level after the entity has already been
		// created, so we store the events for now but account for it when iterating over them later.
		commandMonitor := &event.CommandMonitor{
			Started:   entity.processStartedEvent,
			Succeeded: entity.processSucceededEvent,
			Failed:    entity.processFailedEvent,
		}

		poolMonitor := &event.PoolMonitor{
			Event: entity.processPoolEvent,
		}

		serverMonitor := &event.ServerMonitor{
			ServerDescriptionChanged:   entity.processServerDescriptionChangedEvent,
			ServerHeartbeatFailed:      entity.processServerHeartbeatFailedEvent,
			ServerHeartbeatStarted:     entity.processServerHeartbeatStartedEvent,
			ServerHeartbeatSucceeded:   entity.processServerHeartbeatSucceededEvent,
			TopologyDescriptionChanged: entity.processTopologyDescriptionChangedEvent,
			TopologyOpening:            entity.processTopologyOpeningEvent,
			TopologyClosed:             entity.processTopologyClosedEvent,
		}

		clientOpts.SetMonitor(commandMonitor).SetPoolMonitor(poolMonitor).SetServerMonitor(serverMonitor)

		for _, eventTypeStr := range entityOptions.ObserveEvents {
			eventType, ok := monitoringEventTypeFromString(eventTypeStr)
			if !ok {
				return nil, fmt.Errorf("unrecognized observed event type %q", eventTypeStr)
			}
			entity.observedEvents[eventType] = struct{}{}
		}
		for _, eventsAsEntity := range entityOptions.StoreEventsAsEntities {
			for _, eventTypeStr := range eventsAsEntity.Events {
				eventType, ok := monitoringEventTypeFromString(eventTypeStr)
				if !ok {
					return nil, fmt.Errorf("unrecognized stored event type %q", eventTypeStr)
				}
				entity.storedEvents[eventType] = append(entity.storedEvents[eventType], eventsAsEntity.EventListID)
			}
		}
	}
	if entityOptions.ServerAPIOptions != nil {
		if err := entityOptions.ServerAPIOptions.ServerAPIVersion.Validate(); err != nil {
			return nil, err
		}
		clientOpts.SetServerAPIOptions(entityOptions.ServerAPIOptions.ServerAPIOptions)
	} else {
		integtest.AddTestServerAPIVersion(clientOpts)
	}
	if entityOptions.AutoEncryptOpts != nil {
		aeo, err := createAutoEncryptionOptions(entityOptions.AutoEncryptOpts)
		if err != nil {
			return nil, fmt.Errorf("error parsing auto encryption options: %w", err)
		}
		clientOpts.SetAutoEncryptionOptions(aeo)
	}
	for _, cmd := range entityOptions.IgnoredCommands {
		entity.ignoredCommands[cmd] = struct{}{}
	}

	client, err := mongo.Connect(clientOpts)
	if err != nil {
		return nil, fmt.Errorf("error creating mongo.Client: %w", err)
	}

	if entityOptions.AwaitMinPoolSizeMS != nil && *entityOptions.AwaitMinPoolSizeMS > 0 &&
		clientOpts.MinPoolSize != nil && *clientOpts.MinPoolSize > 0 {

		if err := func() error {
			awaitDur := time.Duration(*entityOptions.AwaitMinPoolSizeMS) * time.Millisecond

			awaitCtx, cancel := context.WithTimeout(ctx, awaitDur)
			defer cancel()

			return awaitMinimumPoolSize(awaitCtx, entity, *clientOpts.MinPoolSize)
		}(); err != nil {
			return nil, err
		}
	}

	entity.Client = client
	return entity, nil
}

func getURIForClient(opts *entityOptions) string {
	if mtest.Serverless() || mtest.ClusterTopologyKind() != mtest.LoadBalanced {
		return mtest.ClusterURI()
	}

	// For non-serverless load-balanced deployments, UseMultipleMongoses is used to determine the load balancer URI. If set to false,
	// the LB fronts a single server. If unset or explicitly true, the LB fronts multiple mongos servers.
	switch {
	case opts.UseMultipleMongoses != nil && !*opts.UseMultipleMongoses:
		return mtest.SingleMongosLoadBalancerURI()
	default:
		return mtest.MultiMongosLoadBalancerURI()
	}
}

// TODO(GODRIVER-3726): Need to update the logic to an Unmarshal method.
func createAutoEncryptionOptions(opts bson.Raw) (*options.AutoEncryptionOptions, error) {
	aeo := options.AutoEncryption()
	var kvnsFound bool
	elems, err := opts.Elements()
	if err != nil {
		return nil, err
	}

	for _, elem := range elems {
		name := elem.Key()
		opt := elem.Value()

		switch name {
		case "kmsProviders":
			providers := make(map[string]map[string]any)
			elems, err := opt.Document().Elements()
			if err != nil {
				return nil, err
			}
			for _, elem := range elems {
				key := elem.Key()
				opt := elem.Value().Document()
				provider, err := getKmsProvider(key, opt)
				if err != nil {
					return nil, err
				}
				if provider == nil {
					continue
				}
				providers[key] = provider
				if key == "kmip" && tlsClientCertificateKeyFile != "" && tlsCAFile != "" {
					cfg, err := options.BuildTLSConfig(map[string]any{
						"tlsCertificateKeyFile": tlsClientCertificateKeyFile,
						"tlsCAFile":             tlsCAFile,
					})
					if err != nil {
						return nil, fmt.Errorf("error constructing tls config: %w", err)
					}
					aeo.SetTLSConfig(map[string]*tls.Config{
						"kmip": cfg,
					})
				}
			}
			aeo.SetKmsProviders(providers)
		case "schemaMap":
			var schemaMap map[string]any
			err := bson.Unmarshal(opt.Document(), &schemaMap)
			if err != nil {
				return nil, fmt.Errorf("error creating schema map: %v", err)
			}
			aeo.SetSchemaMap(schemaMap)
		case "keyVaultNamespace":
			kvnsFound = true
			aeo.SetKeyVaultNamespace(opt.StringValue())
		case "bypassAutoEncryption":
			aeo.SetBypassAutoEncryption(opt.Boolean())
		case "encryptedFieldsMap":
			var encryptedFieldsMap map[string]any
			err := bson.Unmarshal(opt.Document(), &encryptedFieldsMap)
			if err != nil {
				return nil, fmt.Errorf("error creating encryptedFieldsMap: %v", err)
			}
			aeo.SetEncryptedFieldsMap(encryptedFieldsMap)
		case "bypassQueryAnalysis":
			aeo.SetBypassQueryAnalysis(opt.Boolean())
		default:
			return nil, fmt.Errorf("unrecognized option: %v", name)
		}
	}
	if !kvnsFound {
		aeo.SetKeyVaultNamespace("keyvault.datakeys")
	}

	return aeo, nil
}

// disconnect disconnects the client associated with this entity. It is an
// idempotent operation, unlike the mongo client's disconnect method. This
// property will help avoid unnecessary errors when calling disconnect on a
// client that has already been disconnected, such as the case when the test
// runner is required to run the closure as part of an operation.
func (c *clientEntity) disconnect(ctx context.Context) error {
	if c.disconnected {
		return nil
	}

	if err := c.Disconnect(ctx); err != nil {
		return err
	}

	c.disconnected = true

	return nil
}

func (c *clientEntity) stopListeningForEvents() {
	c.setRecordEvents(false)
}

func (c *clientEntity) isIgnoredEvent(commandName string, eventDoc bson.Raw) bool {
	// Check if command is in ignoredCommands.
	if _, ok := c.ignoredCommands[commandName]; ok {
		return true
	}

	if commandName == "hello" || strings.ToLower(commandName) == handshake.LegacyHelloLowercase {
		// If observeSensitiveCommands is false (or unset) and hello command has been
		// redacted at operation level, hello command should be ignored as it contained
		// speculativeAuthenticate.
		sensitiveCommandsIgnored := c.observeSensitiveCommands == nil || !*c.observeSensitiveCommands
		redacted := len(eventDoc) == 0
		if sensitiveCommandsIgnored && redacted {
			return true
		}
	}
	return false
}

func (c *clientEntity) startedEvents() []*event.CommandStartedEvent {
	var events []*event.CommandStartedEvent
	for _, evt := range c.started {
		if !c.isIgnoredEvent(evt.CommandName, evt.Command) {
			events = append(events, evt)
		}
	}

	return events
}

func (c *clientEntity) succeededEvents() []*event.CommandSucceededEvent {
	var events []*event.CommandSucceededEvent
	for _, evt := range c.succeeded {
		if !c.isIgnoredEvent(evt.CommandName, evt.Reply) {
			events = append(events, evt)
		}
	}

	return events
}

func (c *clientEntity) failedEvents() []*event.CommandFailedEvent {
	var events []*event.CommandFailedEvent
	for _, evt := range c.failed {
		if _, ok := c.ignoredCommands[evt.CommandName]; !ok {
			events = append(events, evt)
		}
	}

	return events
}

func (c *clientEntity) poolEvents() []*event.PoolEvent {
	return c.pooled
}

func (c *clientEntity) numberConnectionsCheckedOut() int32 {
	return c.numConnsCheckedOut
}

func (c *clientEntity) addEventsCount(eventType monitoringEventType) {
	c.eventsCountLock.Lock()
	defer c.eventsCountLock.Unlock()

	c.eventsCount[eventType]++
}

func (c *clientEntity) addServerDescriptionChangedEventCount(evt serverDescriptionChangedEventInfo) {
	c.serverDescriptionChangedEventsCountLock.Lock()
	defer c.serverDescriptionChangedEventsCountLock.Unlock()

	c.serverDescriptionChangedEventsCount[evt]++
}

func (c *clientEntity) getEventCount(eventType monitoringEventType) int32 {
	c.eventsCountLock.RLock()
	defer c.eventsCountLock.RUnlock()

	return c.eventsCount[eventType]
}

func (c *clientEntity) getServerDescriptionChangedEventCount(expected serverDescriptionChangedEventInfo) int32 {
	c.serverDescriptionChangedEventsCountLock.Lock()
	defer c.serverDescriptionChangedEventsCountLock.Unlock()

	// If the previousDescription or newDescription is "nil" in the expected case,
	// then those fields can be anything.
	for actual, count := range c.serverDescriptionChangedEventsCount {
		actPrevD := actual.PreviousDescription
		expPrevD := expected.PreviousDescription

		if expPrevD != nil && expPrevD.Type != actPrevD.Type {
			continue
		}

		actNewD := actual.NewDescription
		expNewD := expected.NewDescription

		if expNewD != nil && expNewD.Type != actNewD.Type {
			continue
		}

		return count
	}

	return 0
}

func getSecondsSinceEpoch() float64 {
	return float64(time.Now().UnixNano()) / float64(time.Second/time.Nanosecond)
}

func (c *clientEntity) processStartedEvent(_ context.Context, evt *event.CommandStartedEvent) {
	if !c.getRecordEvents() {
		return
	}
	if _, ok := c.observedEvents[commandStartedEvent]; ok {
		c.started = append(c.started, evt)
	}

	c.addEventsCount(commandStartedEvent)

	eventListIDs, ok := c.storedEvents[commandStartedEvent]
	if !ok {
		return
	}
	bsonBuilder := bsoncore.NewDocumentBuilder().
		AppendString("name", "CommandStartedEvent").
		AppendDouble("observedAt", getSecondsSinceEpoch()).
		AppendString("databaseName", evt.DatabaseName).
		AppendString("commandName", evt.CommandName).
		AppendInt64("requestId", evt.RequestID).
		AppendString("connectionId", evt.ConnectionID)
	if evt.ServiceID != nil {
		bsonBuilder.AppendString("serviceId", evt.ServiceID.String())
	}
	doc := bson.Raw(bsonBuilder.Build())
	for _, id := range eventListIDs {
		c.entityMap.appendEventsEntity(id, doc)
	}
}

func (c *clientEntity) processSucceededEvent(_ context.Context, evt *event.CommandSucceededEvent) {
	if !c.getRecordEvents() {
		return
	}
	if _, ok := c.observedEvents[commandSucceededEvent]; ok {
		c.succeeded = append(c.succeeded, evt)
	}

	c.addEventsCount(commandSucceededEvent)

	eventListIDs, ok := c.storedEvents["CommandSucceededEvent"]
	if !ok {
		return
	}
	bsonBuilder := bsoncore.NewDocumentBuilder().
		AppendString("name", "CommandSucceededEvent").
		AppendDouble("observedAt", getSecondsSinceEpoch()).
		AppendString("commandName", evt.CommandName).
		AppendInt64("requestId", evt.RequestID).
		AppendString("connectionId", evt.ConnectionID)
	if evt.ServiceID != nil {
		bsonBuilder.AppendString("serviceId", evt.ServiceID.String())
	}
	doc := bson.Raw(bsonBuilder.Build())
	for _, id := range eventListIDs {
		c.entityMap.appendEventsEntity(id, doc)
	}
}

func (c *clientEntity) processFailedEvent(_ context.Context, evt *event.CommandFailedEvent) {
	if !c.getRecordEvents() {
		return
	}
	if _, ok := c.observedEvents[commandFailedEvent]; ok {
		c.failed = append(c.failed, evt)
	}

	c.addEventsCount(commandFailedEvent)

	eventListIDs, ok := c.storedEvents["CommandFailedEvent"]
	if !ok {
		return
	}
	bsonBuilder := bsoncore.NewDocumentBuilder().
		AppendString("name", "CommandFailedEvent").
		AppendDouble("observedAt", getSecondsSinceEpoch()).
		AppendInt64("durationNanos", evt.Duration.Nanoseconds()).
		AppendString("commandName", evt.CommandName).
		AppendInt64("requestId", evt.RequestID).
		AppendString("connectionId", evt.ConnectionID).
		AppendString("failure", evt.Failure.Error())
	if evt.ServiceID != nil {
		bsonBuilder.AppendString("serviceId", evt.ServiceID.String())
	}
	doc := bson.Raw(bsonBuilder.Build())
	for _, id := range eventListIDs {
		c.entityMap.appendEventsEntity(id, doc)
	}
}

func (c *clientEntity) resetEventHistory() {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	c.pooled = nil
	c.serverDescriptionChanged = nil
	c.serverHeartbeatStartedEvent = nil
	c.serverHeartbeatSucceeded = nil
	c.serverHeartbeatFailedEvent = nil
	c.topologyDescriptionChanged = nil
	c.topologyOpening = nil
	c.topologyClosed = nil
}

func (c *clientEntity) latestTopology() event.TopologyDescription {
	c.eventProcessMu.RLock()
	defer c.eventProcessMu.RUnlock()

	return c.latestDesc
}

func getPoolEventDocument(evt *event.PoolEvent, eventType monitoringEventType) bson.Raw {
	bsonBuilder := bsoncore.NewDocumentBuilder().
		AppendString("name", string(eventType)).
		AppendDouble("observedAt", getSecondsSinceEpoch()).
		AppendString("address", evt.Address)
	if evt.ConnectionID != 0 {
		bsonBuilder.AppendString("connectionId", fmt.Sprint(evt.ConnectionID))
	}
	if evt.PoolOptions != nil {
		optionsDoc := bsoncore.NewDocumentBuilder().
			AppendString("maxPoolSize", fmt.Sprint(evt.PoolOptions.MaxPoolSize)).
			AppendString("minPoolSize", fmt.Sprint(evt.PoolOptions.MinPoolSize)).
			AppendString("maxIdleTimeMS", fmt.Sprint(evt.PoolOptions.WaitQueueTimeoutMS)).
			Build()
		bsonBuilder.AppendDocument("poolOptions", optionsDoc)
	}
	if evt.Reason != "" {
		bsonBuilder.AppendString("reason", evt.Reason)
	}
	if evt.ServiceID != nil {
		bsonBuilder.AppendString("serviceId", evt.ServiceID.String())
	}
	return bson.Raw(bsonBuilder.Build())
}

func (c *clientEntity) processPoolEvent(evt *event.PoolEvent) {
	if !c.getRecordEvents() {
		return
	}

	// Update the connection counter. This happens even if we're not storing any
	// events.
	switch evt.Type {
	case event.ConnectionCheckedOut:
		atomic.AddInt32(&c.numConnsCheckedOut, 1)
	case event.ConnectionCheckedIn:
		atomic.AddInt32(&c.numConnsCheckedOut, -1)
	case event.ConnectionReady:
		c.eventProcessMu.Lock()
		c.connsPerServer[evt.Address]++
		c.eventProcessMu.Unlock()
	case event.ConnectionClosed:
		c.eventProcessMu.Lock()
		c.connsPerServer[evt.Address]--
		c.eventProcessMu.Unlock()
	}

	eventType := monitoringEventTypeFromPoolEvent(evt)
	if _, ok := c.observedEvents[eventType]; ok {
		c.pooled = append(c.pooled, evt)
	}

	c.addEventsCount(eventType)

	if eventListIDs, ok := c.storedEvents[eventType]; ok {
		eventBSON := getPoolEventDocument(evt, eventType)
		for _, id := range eventListIDs {
			c.entityMap.appendEventsEntity(id, eventBSON)
		}
	}
}

// connsReady returns the number of ready connections for the given server
// address. If the server is not data-bearing, this method will return -1.
func (c *clientEntity) connsReady(server event.ServerDescription) int {
	c.eventProcessMu.RLock()
	defer c.eventProcessMu.RUnlock()

	if server.Kind == description.ServerKindRSArbiter.String() ||
		server.Kind == description.ServerKindRSGhost.String() {
		return -1
	}

	return c.connsPerServer[server.Addr.String()]
}

func (c *clientEntity) processServerDescriptionChangedEvent(evt *event.ServerDescriptionChangedEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	if _, ok := c.observedEvents[serverDescriptionChangedEvent]; ok {
		c.serverDescriptionChanged = append(c.serverDescriptionChanged, evt)
	}

	// Record object-specific unified spec test data on an event.
	c.addServerDescriptionChangedEventCount(*newServerDescriptionChangedEventInfo(evt))

	// Record the event generally.
	c.addEventsCount(serverDescriptionChangedEvent)
}

func (c *clientEntity) processServerHeartbeatFailedEvent(evt *event.ServerHeartbeatFailedEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	if _, ok := c.observedEvents[serverHeartbeatFailedEvent]; ok {
		c.serverHeartbeatFailedEvent = append(c.serverHeartbeatFailedEvent, evt)
	}

	c.addEventsCount(serverHeartbeatFailedEvent)
}

func (c *clientEntity) processServerHeartbeatStartedEvent(evt *event.ServerHeartbeatStartedEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	if _, ok := c.observedEvents[serverHeartbeatStartedEvent]; ok {
		c.serverHeartbeatStartedEvent = append(c.serverHeartbeatStartedEvent, evt)
	}

	c.addEventsCount(serverHeartbeatStartedEvent)
}

func (c *clientEntity) processServerHeartbeatSucceededEvent(evt *event.ServerHeartbeatSucceededEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	if _, ok := c.observedEvents[serverHeartbeatSucceededEvent]; ok {
		c.serverHeartbeatSucceeded = append(c.serverHeartbeatSucceeded, evt)
	}

	c.addEventsCount(serverHeartbeatSucceededEvent)
}

func (c *clientEntity) processTopologyDescriptionChangedEvent(evt *event.TopologyDescriptionChangedEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	c.latestDesc = evt.NewDescription

	if _, ok := c.observedEvents[topologyDescriptionChangedEvent]; ok {
		c.topologyDescriptionChanged = append(c.topologyDescriptionChanged, evt)
	}

	c.addEventsCount(topologyDescriptionChangedEvent)
}

func (c *clientEntity) processTopologyOpeningEvent(evt *event.TopologyOpeningEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	if _, ok := c.observedEvents[topologyOpeningEvent]; ok {
		c.topologyOpening = append(c.topologyOpening, evt)
	}

	c.addEventsCount(topologyOpeningEvent)
}

func (c *clientEntity) processTopologyClosedEvent(evt *event.TopologyClosedEvent) {
	c.eventProcessMu.Lock()
	defer c.eventProcessMu.Unlock()

	if !c.getRecordEvents() {
		return
	}

	if _, ok := c.observedEvents[topologyClosedEvent]; ok {
		c.topologyClosed = append(c.topologyClosed, evt)
	}

	c.addEventsCount(topologyClosedEvent)
}

func (c *clientEntity) setRecordEvents(record bool) {
	c.recordEvents.Store(record)
}

func (c *clientEntity) getRecordEvents() bool {
	return c.recordEvents.Load().(bool)
}

func setClientOptionsFromURIOptions(clientOpts *options.ClientOptions, uriOpts bson.M) error {
	// A write concern can be constructed across multiple URI options (e.g. "w", "j", and "wTimeoutMS") so we declare an
	// empty writeConcern instance here that can be populated in the loop below.
	var wc writeConcern
	var wcSet bool

	for key, value := range uriOpts {
		switch strings.ToLower(key) {
		case "appname":
			clientOpts.SetAppName(value.(string))
		case "connecttimeoutms":
			clientOpts.SetConnectTimeout(time.Duration(value.(int32)) * time.Millisecond)
		case "heartbeatfrequencyms":
			clientOpts.SetHeartbeatInterval(time.Duration(value.(int32)) * time.Millisecond)
		case "loadbalanced":
			clientOpts.SetLoadBalanced(value.(bool))
		case "maxidletimems":
			clientOpts.SetMaxConnIdleTime(time.Duration(value.(int32)) * time.Millisecond)
		case "minpoolsize":
			clientOpts.SetMinPoolSize(uint64(value.(int32)))
		case "maxpoolsize":
			clientOpts.SetMaxPoolSize(uint64(value.(int32)))
		case "maxconnecting":
			clientOpts.SetMaxConnecting(uint64(value.(int32)))
		case "readconcernlevel":
			clientOpts.SetReadConcern(&readconcern.ReadConcern{Level: value.(string)})
		case "retryreads":
			clientOpts.SetRetryReads(value.(bool))
		case "retrywrites":
			clientOpts.SetRetryWrites(value.(bool))
		case "sockettimeoutms":
			// TODO(DRIVERS-2829): Error here instead of skip to ensure that if new
			// tests are added containing socketTimeoutMS (a legacy timeout option
			// that we have removed as of v2), then a CSOT analogue exists. Once we
			// have ensured an analogue exists, extend "skippedTestDescriptions" to
			// avoid this error.
			return newSkipTestError("the socketTimeoutMS client option is not supported")
		case "w":
			wc.W = value
			wcSet = true
		case "waitqueuetimeoutms":
			return newSkipTestError("the waitQueueTimeoutMS client option is not supported")
		case "waitqueuesize":
			return newSkipTestError("the waitQueueSize client option is not supported")
		case "timeoutms":
			clientOpts.SetTimeout(time.Duration(value.(int32)) * time.Millisecond)
		case "serverselectiontimeoutms":
			clientOpts.SetServerSelectionTimeout(time.Duration(value.(int32)) * time.Millisecond)
		case "servermonitoringmode":
			clientOpts.SetServerMonitoringMode(value.(string))
		default:
			return fmt.Errorf("unrecognized URI option %s", key)
		}
	}

	if wcSet {
		converted, err := wc.toWriteConcernOption()
		if err != nil {
			return fmt.Errorf("error creating write concern: %w", err)
		}
		clientOpts.SetWriteConcern(converted)
	}
	return nil
}

func evaluateUseMultipleMongoses(clientOpts *options.ClientOptions, useMultipleMongoses bool) error {
	hosts := mtest.ClusterConnString().Hosts

	if !useMultipleMongoses {
		clientOpts.SetHosts(hosts[:1])
		return nil
	}

	if len(hosts) < 2 {
		return fmt.Errorf("multiple mongoses required but cluster URI %q only contains one host", mtest.ClusterURI())
	}
	return nil
}

// awaitMinimumPoolSize waits for the client's connection pool to reach the
// specified minimum size. This is a best effort operation that times out after
// some predefined amount of time to avoid blocking tests indefinitely.
func awaitMinimumPoolSize(ctx context.Context, entity *clientEntity, minPoolSize uint64) error {
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return fmt.Errorf("timed out waiting for client to reach minPoolSize")
		case <-ticker.C:
			ready := true
			for _, server := range entity.latestTopology().Servers {
				if r := entity.connsReady(server); r >= 0 && r < int(minPoolSize) {
					ready = false

					// If any server has less than minPoolSize connections, continue
					// waiting.
					break
				}
			}

			if ready {
				entity.resetEventHistory()

				return nil
			}
		}
	}
}
