// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package raft

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"io"
	"math/rand"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/go-test/deep"
	"github.com/golang/protobuf/proto"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-secure-stdlib/base62"
	"github.com/hashicorp/go-uuid"
	"github.com/hashicorp/raft"
	"github.com/hashicorp/vault/sdk/helper/jsonutil"
	"github.com/hashicorp/vault/sdk/physical"
	"github.com/stretchr/testify/require"
	bolt "go.etcd.io/bbolt"
)

func testBothRaftBackends(t *testing.T, f func(t *testing.T, raftWALValue string)) {
	t.Helper()

	testCases := []struct {
		name   string
		useWAL string
	}{
		{
			name:   "use wal",
			useWAL: "true",
		},
		{
			name:   "use boltdb",
			useWAL: "false",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// we can't use t.Parallel() here because some raft tests manipulate package level variables
			f(t, tc.useWAL)
		})
	}
}

func testBothRaftBackendsBenchmark(b *testing.B, f func(raftWALValue string)) {
	testCases := []struct {
		name   string
		useWAL string
	}{
		{
			name:   "use wal",
			useWAL: "true",
		},
		{
			name:   "use boltdb",
			useWAL: "false",
		},
	}

	for _, tc := range testCases {
		b.Run(tc.name, func(b *testing.B) {
			f(tc.useWAL)
		})
	}
}

func connectPeers(nodes ...*RaftBackend) {
	for _, node := range nodes {
		for _, peer := range nodes {
			if node == peer {
				continue
			}

			node.raftTransport.(*raft.InmemTransport).Connect(raft.ServerAddress(peer.NodeID()), peer.raftTransport)
			peer.raftTransport.(*raft.InmemTransport).Connect(raft.ServerAddress(node.NodeID()), node.raftTransport)
		}
	}
}

func stepDownLeader(t *testing.T, node *RaftBackend) {
	t.Helper()

	if err := node.raft.LeadershipTransfer().Error(); err != nil {
		t.Fatal(err)
	}

	timeout := time.Now().Add(time.Second * 10)
	for !time.Now().After(timeout) {
		if err := node.raft.VerifyLeader().Error(); err != nil {
			return
		}
		time.Sleep(100 * time.Millisecond)
	}

	t.Fatal("still leader")
}

func waitForLeader(t *testing.T, nodes ...*RaftBackend) *RaftBackend {
	t.Helper()
	timeout := time.Now().Add(time.Second * 10)
	for !time.Now().After(timeout) {
		for _, node := range nodes {
			if node.raft.Leader() == raft.ServerAddress(node.NodeID()) {
				return node
			}
		}
		time.Sleep(100 * time.Millisecond)
	}

	t.Fatal("no leader")
	return nil
}

func compareFSMs(t *testing.T, fsm1, fsm2 *FSM) {
	t.Helper()
	if err := compareFSMsWithErr(t, fsm1, fsm2); err != nil {
		t.Fatal(err)
	}
}

func compareFSMsWithErr(t *testing.T, fsm1, fsm2 *FSM) error {
	t.Helper()
	index1, config1 := fsm1.LatestState()
	index2, config2 := fsm2.LatestState()

	if !proto.Equal(index1, index2) {
		return fmt.Errorf("indexes did not match: %+v != %+v", index1, index2)
	}
	if !proto.Equal(config1, config2) {
		return fmt.Errorf("configs did not match: %+v != %+v", config1, config2)
	}

	return compareDBs(t, fsm1.getDB(), fsm2.getDB(), false)
}

func compareDBs(t *testing.T, boltDB1, boltDB2 *bolt.DB, dataOnly bool) error {
	t.Helper()
	db1 := make(map[string]string)
	db2 := make(map[string]string)

	err := boltDB1.View(func(tx *bolt.Tx) error {
		c := tx.Cursor()
		for bucketName, _ := c.First(); bucketName != nil; bucketName, _ = c.Next() {
			if dataOnly && !bytes.Equal(bucketName, dataBucketName) {
				continue
			}

			b := tx.Bucket(bucketName)

			cBucket := b.Cursor()

			for k, v := cBucket.First(); k != nil; k, v = cBucket.Next() {
				db1[string(k)] = base64.StdEncoding.EncodeToString(v)
			}
		}

		return nil
	})
	if err != nil {
		t.Fatal(err)
	}

	err = boltDB2.View(func(tx *bolt.Tx) error {
		c := tx.Cursor()
		for bucketName, _ := c.First(); bucketName != nil; bucketName, _ = c.Next() {
			if dataOnly && !bytes.Equal(bucketName, dataBucketName) {
				continue
			}
			b := tx.Bucket(bucketName)

			c := b.Cursor()

			for k, v := c.First(); k != nil; k, v = c.Next() {
				db2[string(k)] = base64.StdEncoding.EncodeToString(v)
			}
		}

		return nil
	})
	if err != nil {
		t.Fatal(err)
	}

	if diff := deep.Equal(db1, db2); diff != nil {
		return fmt.Errorf("%+v", diff)
	}

	return nil
}

func TestRaft_Backend(t *testing.T) {
	t.Parallel()

	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		physical.ExerciseBackend(t, b)
	})
}

// TestRaft_SwitchFromBoltDBToRaftWal is testing that we don't use raft-wal, even if configured to do so,
// if there is an existing raft.db file on disk (meaning BoltDB was previously in use).
func TestRaft_SwitchFromBoltDBToRaftWal(t *testing.T) {
	tmpDir := t.TempDir()

	// configured to use raft-wal
	conf := map[string]string{
		"path":          tmpDir,
		"trailing_logs": "100",
		"raft_wal":      "true",
	}

	// raftBaseDir will end up looking like $tmpDir/raft
	raftBaseDir := filepath.Join(tmpDir, raftState)
	err := os.MkdirAll(raftBaseDir, 0o777)
	if err != nil {
		t.Fatal(err)
	}

	// create a bogus $tmpDir/raft/raft.db file
	db, err := bolt.Open(filepath.Join(raftBaseDir, "raft.db"), 0o777, nil)
	if err != nil {
		t.Fatal(err)
	}
	err = db.Close()
	if err != nil {
		t.Fatal(err)
	}

	_, err = NewRaftBackend(conf, hclog.NewNullLogger())
	if err != nil {
		t.Fatal(err)
	}

	// Check to see if $tmpDir/raft/raft-wal exists. It should not, because we only create that if raft-wal is in use.
	// And since raft.db already existed, we should've skipped all the raft-wal setup code.
	raftWalExists, err := fileExists(filepath.Join(raftBaseDir, raftWalDir))
	if err != nil {
		t.Fatal(err)
	}

	if raftWalExists {
		t.Fatal("expected raft-wal dir to not exist, but it does")
	}
}

// TestRaft_VerifierEnabled is not checking to ensure that the verifier works correctly - the verifier has
// its own unit tests for that. What we're checking for here is that we've plumbed everything through correctly,
// i.e. we can stand up a raft cluster with the verifier enabled, do a bunch of raft things, let the verifier
// do its thing, and nothing blows up.
func TestRaft_VerifierEnabled(t *testing.T) {
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs":             "100",
			"raft_wal":                  useRaftWal,
			"raft_log_verifier_enabled": "true",
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		physical.ExerciseBackend(t, b)

		err := b.applyVerifierCheckpoint()
		if err != nil {
			t.Fatal(err)
		}
		physical.ExerciseBackend(t, b)
	})
}

func TestRaft_Backend_LargeKey(t *testing.T) {
	t.Parallel()

	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		key, err := base62.Random(bolt.MaxKeySize + 1)
		if err != nil {
			t.Fatal(err)
		}
		entry := &physical.Entry{Key: key, Value: []byte(key)}

		err = b.Put(context.Background(), entry)
		if err == nil {
			t.Fatal("expected error for put entry")
		}

		if !strings.Contains(err.Error(), physical.ErrKeyTooLarge) {
			t.Fatalf("expected %q, got %v", physical.ErrKeyTooLarge, err)
		}

		out, err := b.Get(context.Background(), entry.Key)
		if err != nil {
			t.Fatalf("unexpected error after failed put: %v", err)
		}
		if out != nil {
			t.Fatal("expected response entry to be nil after a failed put")
		}
	})
}

func TestRaft_Backend_LargeValue(t *testing.T) {
	t.Parallel()

	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		value := make([]byte, defaultMaxEntrySize+1)
		rand.Read(value)
		entry := &physical.Entry{Key: "foo", Value: value}

		err := b.Put(context.Background(), entry)
		if err == nil {
			t.Fatal("expected error for put entry")
		}

		if !strings.Contains(err.Error(), physical.ErrValueTooLarge) {
			t.Fatalf("expected %q, got %v", physical.ErrValueTooLarge, err)
		}

		out, err := b.Get(context.Background(), entry.Key)
		if err != nil {
			t.Fatalf("unexpected error after failed put: %v", err)
		}
		if out != nil {
			t.Fatal("expected response entry to be nil after a failed put")
		}
	})
}

// TestRaft_TransactionalBackend_GetTransactions tests that passing a slice of transactions to the
// raft backend will populate values for any transactions that are Get operations.
func TestRaft_TransactionalBackend_GetTransactions(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		ctx := context.Background()
		txns := make([]*physical.TxnEntry, 0)

		// Add some seed values to our FSM, and prepare our slice of transactions at the same time
		for i := 0; i < 5; i++ {
			key := fmt.Sprintf("foo/%d", i)
			err := b.fsm.Put(ctx, &physical.Entry{Key: key, Value: []byte(fmt.Sprintf("value-%d", i))})
			if err != nil {
				t.Fatal(err)
			}

			txns = append(txns, &physical.TxnEntry{
				Operation: physical.GetOperation,
				Entry: &physical.Entry{
					Key: key,
				},
			})
		}

		// Add some additional transactions, so we have a mix of operations
		for i := 0; i < 10; i++ {
			txnEntry := &physical.TxnEntry{
				Entry: &physical.Entry{
					Key: fmt.Sprintf("lol-%d", i),
				},
			}

			if i%2 == 0 {
				txnEntry.Operation = physical.PutOperation
				txnEntry.Entry.Value = []byte("lol")
			} else {
				txnEntry.Operation = physical.DeleteOperation
			}

			txns = append(txns, txnEntry)
		}

		err := b.Transaction(ctx, txns)
		if err != nil {
			t.Fatal(err)
		}

		// Check that our Get operations were populated with their values
		for i, txn := range txns {
			if txn.Operation == physical.GetOperation {
				val := []byte(fmt.Sprintf("value-%d", i))
				if !bytes.Equal(val, txn.Entry.Value) {
					t.Fatalf("expected %s to equal %s but it didn't", hex.EncodeToString(val), hex.EncodeToString(txn.Entry.Value))
				}
			}
		}
	})
}

func TestRaft_TransactionalBackend_LargeKey(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		value := make([]byte, defaultMaxEntrySize+1)
		rand.Read(value)

		key, err := base62.Random(bolt.MaxKeySize + 1)
		if err != nil {
			t.Fatal(err)
		}
		txns := []*physical.TxnEntry{
			{
				Operation: physical.PutOperation,
				Entry: &physical.Entry{
					Key:   key,
					Value: []byte(key),
				},
			},
		}

		err = b.Transaction(context.Background(), txns)
		if err == nil {
			t.Fatal("expected error for transactions")
		}

		if !strings.Contains(err.Error(), physical.ErrKeyTooLarge) {
			t.Fatalf("expected %q, got %v", physical.ErrValueTooLarge, err)
		}

		out, err := b.Get(context.Background(), txns[0].Entry.Key)
		if err != nil {
			t.Fatalf("unexpected error after failed put: %v", err)
		}
		if out != nil {
			t.Fatal("expected response entry to be nil after a failed put")
		}
	})
}

func TestRaft_TransactionalBackend_LargeValue(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		value := make([]byte, defaultMaxEntrySize+1)
		rand.Read(value)

		txns := []*physical.TxnEntry{
			{
				Operation: physical.PutOperation,
				Entry: &physical.Entry{
					Key:   "foo",
					Value: value,
				},
			},
		}

		err := b.Transaction(context.Background(), txns)
		if err == nil {
			t.Fatal("expected error for transactions")
		}

		if !strings.Contains(err.Error(), physical.ErrValueTooLarge) {
			t.Fatalf("expected %q, got %v", physical.ErrValueTooLarge, err)
		}

		out, err := b.Get(context.Background(), txns[0].Entry.Key)
		if err != nil {
			t.Fatalf("unexpected error after failed put: %v", err)
		}
		if out != nil {
			t.Fatal("expected response entry to be nil after a failed put")
		}
	})
}

func TestRaft_Backend_ListPrefix(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		physical.ExerciseBackend_ListPrefix(t, b)
	})
}

func TestRaft_TransactionalBackend(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, _ := GetRaftWithConfig(t, true, true, conf)
		physical.ExerciseTransactionalBackend(t, b)
	})
}

func TestRaft_HABackend(t *testing.T) {
	t.Skip()
	raft1, _ := GetRaft(t, true, true)
	raft2, _ := GetRaft(t, false, true)

	// Add raft2 to the cluster
	addPeer(t, raft1, raft2)
	physical.ExerciseHABackend(t, raft1, raft2)
}

func TestRaft_Backend_ThreeNode(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		raft1, _ := GetRaftWithConfig(t, true, true, conf)
		raft2, _ := GetRaftWithConfig(t, false, true, conf)
		raft3, _ := GetRaftWithConfig(t, false, true, conf)

		// Add raft2 to the cluster
		addPeer(t, raft1, raft2)

		// Add raft3 to the cluster
		addPeer(t, raft1, raft3)

		physical.ExerciseBackend(t, raft1)

		time.Sleep(10 * time.Second)
		// Make sure all stores are the same
		compareFSMs(t, raft1.fsm, raft2.fsm)
		compareFSMs(t, raft1.fsm, raft3.fsm)
	})
}

func TestRaft_GetOfflineConfig(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		config := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		// Create 3 raft nodes
		raft1, _ := GetRaftWithConfig(t, true, true, config)
		raft2, _ := GetRaftWithConfig(t, false, true, config)
		raft3, _ := GetRaftWithConfig(t, false, true, config)

		// Add them all to the cluster
		addPeer(t, raft1, raft2)
		addPeer(t, raft1, raft3)

		// Add some data into the FSM
		physical.ExerciseBackend(t, raft1)

		time.Sleep(10 * time.Second)

		// Spin down the raft cluster and check that GetConfigurationOffline
		// returns 3 voters
		err := raft3.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}
		err = raft2.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}
		err = raft1.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}

		conf, err := raft1.GetConfigurationOffline()
		if err != nil {
			t.Fatal(err)
		}
		if len(conf.Servers) != 3 {
			t.Fatalf("three raft nodes existed but we only see %d", len(conf.Servers))
		}
		for _, s := range conf.Servers {
			if s.Voter != true {
				t.Fatalf("one of the nodes is not a voter")
			}
		}
	})
}

func TestRaft_Recovery(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		// Create 4 raft nodes
		raft1, dir1 := GetRaftWithConfig(t, true, true, conf)
		raft2, dir2 := GetRaftWithConfig(t, false, true, conf)
		raft3, _ := GetRaftWithConfig(t, false, true, conf)
		raft4, dir4 := GetRaftWithConfig(t, false, true, conf)

		// Add them all to the cluster
		addPeer(t, raft1, raft2)
		addPeer(t, raft1, raft3)
		addPeer(t, raft1, raft4)

		// Add some data into the FSM
		physical.ExerciseBackend(t, raft1)

		time.Sleep(10 * time.Second)

		// Bring down all nodes
		err := raft1.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}
		err = raft2.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}
		err = raft3.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}
		err = raft4.TeardownCluster(nil)
		if err != nil {
			t.Fatal(err)
		}

		// Prepare peers.json
		type RecoveryPeer struct {
			ID       string `json:"id"`
			Address  string `json:"address"`
			NonVoter bool   `json:"non_voter"`
		}

		// Leave out node 1 during recovery
		peersList := make([]*RecoveryPeer, 0, 3)
		peersList = append(peersList, &RecoveryPeer{
			ID:       raft1.NodeID(),
			Address:  raft1.NodeID(),
			NonVoter: false,
		})
		peersList = append(peersList, &RecoveryPeer{
			ID:       raft2.NodeID(),
			Address:  raft2.NodeID(),
			NonVoter: false,
		})
		peersList = append(peersList, &RecoveryPeer{
			ID:       raft4.NodeID(),
			Address:  raft4.NodeID(),
			NonVoter: false,
		})

		peersJSONBytes, err := jsonutil.EncodeJSON(peersList)
		if err != nil {
			t.Fatal(err)
		}
		err = os.WriteFile(filepath.Join(filepath.Join(dir1, raftState), "peers.json"), peersJSONBytes, 0o644)
		if err != nil {
			t.Fatal(err)
		}
		err = os.WriteFile(filepath.Join(filepath.Join(dir2, raftState), "peers.json"), peersJSONBytes, 0o644)
		if err != nil {
			t.Fatal(err)
		}
		err = os.WriteFile(filepath.Join(filepath.Join(dir4, raftState), "peers.json"), peersJSONBytes, 0o644)
		if err != nil {
			t.Fatal(err)
		}

		// Bring up the nodes again
		err = raft1.SetupCluster(context.Background(), SetupOpts{})
		if err != nil {
			t.Fatal(err)
		}
		err = raft2.SetupCluster(context.Background(), SetupOpts{})
		if err != nil {
			t.Fatal(err)
		}
		err = raft4.SetupCluster(context.Background(), SetupOpts{})
		if err != nil {
			t.Fatal(err)
		}

		peers, err := raft1.Peers(context.Background())
		if err != nil {
			t.Fatal(err)
		}
		if len(peers) != 3 {
			t.Fatalf("failed to recover the cluster")
		}

		time.Sleep(10 * time.Second)

		compareFSMs(t, raft1.fsm, raft2.fsm)
		compareFSMs(t, raft1.fsm, raft4.fsm)
	})
}

func TestRaft_TransactionalBackend_ThreeNode(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		raft1, _ := GetRaftWithConfig(t, true, true, conf)
		raft2, _ := GetRaftWithConfig(t, false, true, conf)
		raft3, _ := GetRaftWithConfig(t, false, true, conf)

		// Add raft2 to the cluster
		addPeer(t, raft1, raft2)

		// Add raft3 to the cluster
		addPeer(t, raft1, raft3)

		physical.ExerciseTransactionalBackend(t, raft1)

		time.Sleep(10 * time.Second)
		// Make sure all stores are the same
		compareFSMs(t, raft1.fsm, raft2.fsm)
		compareFSMs(t, raft1.fsm, raft3.fsm)
	})
}

// TestRaft_Removed creates a 3 node cluster and checks that the nodes are not
// removed, then verifies that node3 marks itself as removed when it gets
// removed from the cluster
func TestRaft_Removed(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, raftWALValue string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      raftWALValue,
		}

		raft1, _ := GetRaftWithConfig(t, true, true, conf)
		raft2, _ := GetRaftWithConfig(t, false, true, conf)
		raft3, _ := GetRaftWithConfig(t, false, true, conf)

		addPeer(t, raft1, raft2)
		addPeer(t, raft1, raft3)
		physical.ExerciseBackend(t, raft1)

		commitIdx := raft1.CommittedIndex()
		ensureCommitApplied(t, commitIdx, raft2)
		ensureCommitApplied(t, commitIdx, raft3)

		require.False(t, raft1.IsRemoved())
		require.False(t, raft2.IsRemoved())
		require.False(t, raft3.IsRemoved())

		callbackCalled := atomic.Bool{}
		raft3.SetRemovedCallback(func() {
			callbackCalled.Store(true)
		})
		err := raft1.RemovePeer(context.Background(), raft3.NodeID())
		require.NoError(t, err)

		require.Eventually(t, raft3.IsRemoved, 15*time.Second, 500*time.Millisecond)
		require.True(t, callbackCalled.Load())
		require.False(t, raft1.IsRemoved())
		require.False(t, raft2.IsRemoved())
	})
}

// TestRaft_TransactionalLimitsEnvOverride ensures the ENV var overrides for
// transaction size limits are plumbed through as expected.
func TestRaft_TransactionalLimitsEnvOverride(t *testing.T) {
	tc := []struct {
		name        string
		envEntries  string
		envSize     string
		wantEntries int
		wantSize    int
		wantLog     string
	}{
		{
			name:        "defaults",
			wantEntries: defaultMaxBatchEntries,
			wantSize:    defaultMaxBatchSize,
		},
		{
			name:        "valid env",
			envEntries:  "123",
			envSize:     "456",
			wantEntries: 123,
			wantSize:    456,
		},
		{
			name:        "invalid entries",
			envEntries:  "not-a-number",
			envSize:     "100",
			wantEntries: defaultMaxBatchEntries,
			wantSize:    100,
			wantLog:     "failed to parse VAULT_RAFT_MAX_BATCH_ENTRIES",
		},
		{
			name:        "invalid entries",
			envEntries:  "100",
			envSize:     "asdasdsasd",
			wantEntries: 100,
			wantSize:    defaultMaxBatchSize,
			wantLog:     "failed to parse VAULT_RAFT_MAX_BATCH_SIZE_BYTES",
		},
		{
			name:        "zero entries",
			envEntries:  "0",
			envSize:     "100",
			wantEntries: defaultMaxBatchEntries,
			wantSize:    100,
			wantLog:     "failed to parse VAULT_RAFT_MAX_BATCH_ENTRIES as an integer > 0",
		},
		{
			name:        "zero size",
			envEntries:  "100",
			envSize:     "0",
			wantEntries: 100,
			wantSize:    defaultMaxBatchSize,
			wantLog:     "failed to parse VAULT_RAFT_MAX_BATCH_SIZE_BYTES as an integer > 0",
		},
	}

	for _, tt := range tc {
		t.Run(tt.name, func(t *testing.T) {
			// Set the env vars within this test
			if tt.envEntries != "" {
				t.Setenv(EnvVaultRaftMaxBatchEntries, tt.envEntries)
			}
			if tt.envSize != "" {
				t.Setenv(EnvVaultRaftMaxBatchSizeBytes, tt.envSize)
			}

			var logBuf bytes.Buffer
			raft1, dir := GetRaftWithLogOutput(t, false, true, &logBuf)
			defer os.RemoveAll(dir)

			e, s := raft1.TransactionLimits()

			require.Equal(t, tt.wantEntries, e)
			require.Equal(t, tt.wantSize, s)
			if tt.wantLog != "" {
				require.Contains(t, logBuf.String(), tt.wantLog)
			}
		})
	}
}

func TestRaft_Backend_Performance(t *testing.T) {
	t.Parallel()
	testBothRaftBackends(t, func(t *testing.T, useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		b, dir := GetRaftWithConfig(t, true, true, conf)

		defaultConfig := raft.DefaultConfig()
		localConfig := raft.DefaultConfig()
		err := ApplyConfigSettings(b.logger, b.conf, localConfig)
		if err != nil {
			t.Fatal(err)
		}

		if localConfig.ElectionTimeout != defaultConfig.ElectionTimeout*5 {
			t.Fatalf("bad config: %v", localConfig)
		}
		if localConfig.HeartbeatTimeout != defaultConfig.HeartbeatTimeout*5 {
			t.Fatalf("bad config: %v", localConfig)
		}
		if localConfig.LeaderLeaseTimeout != defaultConfig.LeaderLeaseTimeout*5 {
			t.Fatalf("bad config: %v", localConfig)
		}

		b.conf = map[string]string{
			"path":                   dir,
			"performance_multiplier": "5",
		}

		localConfig = raft.DefaultConfig()
		err = ApplyConfigSettings(b.logger, b.conf, localConfig)
		if err != nil {
			t.Fatal(err)
		}

		if localConfig.ElectionTimeout != defaultConfig.ElectionTimeout*5 {
			t.Fatalf("bad config: %v", localConfig)
		}
		if localConfig.HeartbeatTimeout != defaultConfig.HeartbeatTimeout*5 {
			t.Fatalf("bad config: %v", localConfig)
		}
		if localConfig.LeaderLeaseTimeout != defaultConfig.LeaderLeaseTimeout*5 {
			t.Fatalf("bad config: %v", localConfig)
		}

		b.conf = map[string]string{
			"path":                   dir,
			"performance_multiplier": "1",
		}

		localConfig = raft.DefaultConfig()
		err = ApplyConfigSettings(b.logger, b.conf, localConfig)
		if err != nil {
			t.Fatal(err)
		}

		if localConfig.ElectionTimeout != defaultConfig.ElectionTimeout {
			t.Fatalf("bad config: %v", localConfig)
		}
		if localConfig.HeartbeatTimeout != defaultConfig.HeartbeatTimeout {
			t.Fatalf("bad config: %v", localConfig)
		}
		if localConfig.LeaderLeaseTimeout != defaultConfig.LeaderLeaseTimeout {
			t.Fatalf("bad config: %v", localConfig)
		}
	})
}

func BenchmarkDB_Puts(b *testing.B) {
	testBothRaftBackendsBenchmark(b, func(useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		raft1, _ := GetRaftWithConfig(b, true, false, conf)
		raft2, _ := GetRaftWithConfig(b, true, false, conf)

		bench := func(b *testing.B, s physical.Backend, dataSize int) {
			data, err := uuid.GenerateRandomBytes(dataSize)
			if err != nil {
				b.Fatal(err)
			}

			ctx := context.Background()
			pe := &physical.Entry{
				Value: data,
			}
			testName := b.Name()

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				pe.Key = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s-%d", testName, i))))
				err := s.Put(ctx, pe)
				if err != nil {
					b.Fatal(err)
				}
			}
		}

		b.Run("256b", func(b *testing.B) { bench(b, raft1, 256) })
		b.Run("256kb", func(b *testing.B) { bench(b, raft2, 256*1024) })
	})
}

func BenchmarkDB_Snapshot(b *testing.B) {
	testBothRaftBackendsBenchmark(b, func(useRaftWal string) {
		conf := map[string]string{
			"trailing_logs": "100",
			"raft_wal":      useRaftWal,
		}

		raft1, _ := GetRaftWithConfig(b, true, false, conf)
		data, err := uuid.GenerateRandomBytes(256 * 1024)
		if err != nil {
			b.Fatal(err)
		}

		ctx := context.Background()
		pe := &physical.Entry{
			Value: data,
		}
		testName := b.Name()

		for i := 0; i < 100; i++ {
			pe.Key = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s-%d", testName, i))))
			err = raft1.Put(ctx, pe)
			if err != nil {
				b.Fatal(err)
			}
		}

		bench := func(b *testing.B, s *FSM) {
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				pe.Key = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s-%d", testName, i))))
				s.writeTo(ctx, discardCloser{Writer: io.Discard}, discardCloser{Writer: io.Discard})
			}
		}

		b.Run("256kb", func(b *testing.B) { bench(b, raft1.fsm) })
	})
}

type discardCloser struct {
	io.Writer
}

func (d discardCloser) Close() error               { return nil }
func (d discardCloser) CloseWithError(error) error { return nil }
