// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package nomad

import (
	"fmt"
	"strconv"
	"strings"
	"testing"

	msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
	"github.com/hashicorp/nomad/acl"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/state"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/testutil"
	"github.com/shoenig/test/must"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/slices"
)

const jobIndex = 1000

func registerMockJob(s *Server, t *testing.T, prefix string, counter int) *structs.Job {
	job := mock.Job()
	job.ID = prefix + strconv.Itoa(counter)
	registerJob(s, t, job)
	return job
}

func registerJob(s *Server, t *testing.T, job *structs.Job) {
	fsmState := s.fsm.State()
	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, jobIndex, nil, job))
}

func mockAlloc() *structs.Allocation {
	a := mock.Alloc()
	a.Name = fmt.Sprintf("%s.%s[%d]", a.Job.Name, "web", 0)
	return a
}

func TestSearch_PrefixSearch_Job(t *testing.T) {
	ci.Parallel(t)

	prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	job := registerMockJob(s, t, prefix, 0)

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.SearchResponse
	if err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp); err != nil {
		t.Fatalf("err: %v", err)
	}

	require.Len(t, resp.Matches[structs.Jobs], 1)
	require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_PrefixSearch_ACL(t *testing.T) {
	ci.Parallel(t)

	jobID := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"

	s, root, cleanupS := TestACLServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	store := s.fsm.State()

	ns := mock.Namespace()
	ns.Name = "not-allowed"
	must.NoError(t, store.UpsertNamespaces(10, []*structs.Namespace{ns}))

	job := registerMockJob(s, t, jobID, 0)

	variable := mock.VariableEncrypted()
	resp := store.VarSet(1001, &structs.VarApplyStateRequest{
		Op:  structs.VarOpSet,
		Var: variable,
	})
	must.NoError(t, resp.Error)

	plugin := mock.CSIPlugin()
	must.NoError(t, store.UpsertCSIPlugin(1002, plugin))

	node := mock.Node()
	must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1003, node))

	disallowedVariable := mock.VariableEncrypted()
	disallowedVariable.Namespace = "not-allowed"
	resp = store.VarSet(2001, &structs.VarApplyStateRequest{
		Op:  structs.VarOpSet,
		Var: disallowedVariable,
	})
	must.NoError(t, resp.Error)

	req := &structs.SearchRequest{
		Prefix:  "",
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	// Try without a token and expect failure
	{
		var resp structs.SearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with an invalid token and expect failure
	{
		invalidToken := mock.CreatePolicyAndToken(t, store, 1003, "test-invalid",
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
		req.AuthToken = invalidToken.SecretID
		var resp structs.SearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect failure due to Jobs being the context
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect success due to All context
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])

		// Jobs, Plugins, and Variables filtered out since token only has access
		// to node:read
		must.SliceEmpty(t, resp.Matches[structs.Jobs])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])
		must.SliceEmpty(t, resp.Matches[structs.Variables])

		must.Eq(t, uint64(1003), resp.Index) // index of node
	}

	// Try with a valid token for namespace:read-job
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1009, "test-valid2",
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{job.ID}, resp.Matches[structs.Jobs])

		// Nodes, Plugins, and Variables filtered out since token only has
		// access to namespace:read-job
		must.SliceEmpty(t, resp.Matches[structs.Nodes])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])
		must.SliceEmpty(t, resp.Matches[structs.Variables])

		// Index of job because all other contexts are filtered out
		must.Eq(t, uint64(1000), resp.Index)
	}

	// Try with a valid token for node:read and namespace:read-job
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1011, "test-valid3", strings.Join([]string{
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
			mock.NodePolicy(acl.PolicyRead),
		}, "\n"))
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{job.ID}, resp.Matches[structs.Jobs])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])
		must.SliceEmpty(t, resp.Matches[structs.Variables])
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
		must.Eq(t, uint64(1003), resp.Index) // index of node
	}

	// Try with a valid token for node:read and namespace:variable:read
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1012, "test-valid4", strings.Join([]string{
			mock.NamespacePolicyWithVariables(structs.DefaultNamespace, "", []string{},
				map[string][]string{"*": []string{"list"}}),
			mock.NodePolicy(acl.PolicyRead),
		}, "\n"))
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.SliceEmpty(t, resp.Matches[structs.Jobs])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])
		must.Eq(t, []string{variable.Path}, resp.Matches[structs.Variables])
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
		must.Eq(t, uint64(2001), resp.Index) // index of variables
	}

	// Try with a valid token for node:read and namespace:variable:read, wildcard ns
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1012, "test-valid4", strings.Join([]string{
			mock.NamespacePolicyWithVariables(structs.DefaultNamespace, "", []string{},
				map[string][]string{"*": []string{"list"}}),
			mock.NodePolicy(acl.PolicyRead),
		}, "\n"))
		req.AuthToken = validToken.SecretID
		req.Namespace = structs.AllNamespacesSentinel
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.SliceEmpty(t, resp.Matches[structs.Jobs])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])
		must.Eq(t, []string{variable.Path}, resp.Matches[structs.Variables])
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
		must.Eq(t, uint64(2001), resp.Index) // index of variables
	}

	// Try with a management token
	{
		req.AuthToken = root.SecretID
		req.Namespace = structs.DefaultNamespace
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{job.ID}, resp.Matches[structs.Jobs])
		must.Eq(t, []string{plugin.ID}, resp.Matches[structs.Plugins])

		expectVars := []string{variable.Path, disallowedVariable.Path}
		slices.Sort(expectVars)
		slices.Sort(resp.Matches[structs.Variables])
		must.Eq(t, expectVars, resp.Matches[structs.Variables])
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
		must.Eq(t, uint64(2001), resp.Index) // highest index
	}
}

func TestSearch_PrefixSearch_All_JobWithHyphen(t *testing.T) {
	ci.Parallel(t)

	prefix := "example-test-------" // Assert that a job with more than 4 hyphens works

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	// Register a job and an allocation
	job := registerMockJob(s, t, prefix, 0)
	alloc := mockAlloc()
	alloc.JobID = job.ID
	alloc.Namespace = job.Namespace
	summary := mock.JobSummary(alloc.JobID)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJobSummary(999, summary))
	require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))

	req := &structs.SearchRequest{
		Context: structs.All,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	// req.Prefix = "example-te": 9
	for i := 1; i < len(prefix); i++ {
		req.Prefix = prefix[:i]
		var resp structs.SearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
		require.Equal(t, 1, len(resp.Matches[structs.Jobs]))
		require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
		require.EqualValues(t, jobIndex, resp.Index)
	}
}

func TestSearch_PrefixSearch_All_LongJob(t *testing.T) {
	ci.Parallel(t)

	prefix := strings.Repeat("a", 100)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	// Register a job and an allocation
	job := registerMockJob(s, t, prefix, 0)
	alloc := mockAlloc()
	alloc.JobID = job.ID
	summary := mock.JobSummary(alloc.JobID)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJobSummary(999, summary))
	require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.All,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Jobs], 1)
	require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
	require.EqualValues(t, jobIndex, resp.Index)
}

// truncate should limit results to 20
func TestSearch_PrefixSearch_Truncate(t *testing.T) {
	ci.Parallel(t)

	prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	for counter := 0; counter < 25; counter++ {
		registerMockJob(s, t, prefix, counter)
	}

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: "default",
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Jobs], 20)
	require.True(t, resp.Truncations[structs.Jobs])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_PrefixSearch_AllWithJob(t *testing.T) {
	ci.Parallel(t)

	prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})

	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	job := registerMockJob(s, t, prefix, 0)
	eval1 := mock.Eval()
	eval1.ID = job.ID
	require.NoError(t, s.fsm.State().UpsertEvals(structs.MsgTypeTestSetup, 2000, []*structs.Evaluation{eval1}))

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.All,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Jobs], 1)
	require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
	require.Len(t, resp.Matches[structs.Evals], 1)
	require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0])
}

func TestSearch_PrefixSearch_Evals(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	eval1 := mock.Eval()
	require.NoError(t, s.fsm.State().UpsertEvals(structs.MsgTypeTestSetup, 2000, []*structs.Evaluation{eval1}))

	prefix := eval1.ID[:len(eval1.ID)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Evals,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: eval1.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Evals], 1)
	require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0])
	require.False(t, resp.Truncations[structs.Evals])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_PrefixSearch_Allocation(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	alloc := mockAlloc()
	summary := mock.JobSummary(alloc.JobID)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJobSummary(999, summary))
	require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 90, []*structs.Allocation{alloc}))

	prefix := alloc.ID[:len(alloc.ID)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Allocs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: alloc.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Allocs], 1)
	require.Equal(t, alloc.ID, resp.Matches[structs.Allocs][0])
	require.False(t, resp.Truncations[structs.Allocs])
	require.Equal(t, uint64(90), resp.Index)
}

func TestSearch_PrefixSearch_All_UUID(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	alloc := mockAlloc()
	summary := mock.JobSummary(alloc.JobID)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJobSummary(999, summary))
	require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))

	node := mock.Node()
	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, node))

	eval1 := mock.Eval()
	eval1.ID = node.ID
	require.NoError(t, fsmState.UpsertEvals(structs.MsgTypeTestSetup, 1002, []*structs.Evaluation{eval1}))

	req := &structs.SearchRequest{
		Context: structs.All,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: eval1.Namespace,
		},
	}

	for i := 1; i < len(alloc.ID); i++ {
		req.Prefix = alloc.ID[:i]
		var resp structs.SearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
		require.Len(t, resp.Matches[structs.Allocs], 1)
		require.Equal(t, alloc.ID, resp.Matches[structs.Allocs][0])
		require.False(t, resp.Truncations[structs.Allocs])
		require.EqualValues(t, 1002, resp.Index)
	}
}

func TestSearch_PrefixSearch_Node(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	fsmState := s.fsm.State()
	node := mock.Node()

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 100, node))

	prefix := node.ID[:len(node.ID)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Nodes,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: structs.DefaultNamespace,
		},
	}

	var resp structs.SearchResponse
	if err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp); err != nil {
		t.Fatalf("err: %v", err)
	}

	require.Len(t, resp.Matches[structs.Nodes], 1)
	require.Equal(t, node.ID, resp.Matches[structs.Nodes][0])
	require.False(t, resp.Truncations[structs.Nodes])
	require.Equal(t, uint64(100), resp.Index)
}

func TestSearch_PrefixSearch_NodePool(t *testing.T) {
	ci.Parallel(t)

	// Start test server.
	s, cleanupS := TestServer(t, nil)
	defer cleanupS()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	// Populate state with test node pools.
	fsmState := s.fsm.State()
	dev1 := &structs.NodePool{Name: "dev-1"}
	dev2 := &structs.NodePool{Name: "dev-2"}
	prod := &structs.NodePool{Name: "prod"}

	err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{dev1, dev2, prod})
	must.NoError(t, err)

	// Run test cases.
	testCases := []struct {
		name     string
		prefix   string
		context  structs.Context
		expected []string
	}{
		{
			name:     "prefix match",
			prefix:   "dev",
			context:  structs.NodePools,
			expected: []string{dev1.Name, dev2.Name},
		},
		{
			name:     "prefix match - all",
			prefix:   "dev",
			context:  structs.All,
			expected: []string{dev1.Name, dev2.Name},
		},
		{
			name:    "empty prefix",
			prefix:  "",
			context: structs.NodePools,
			expected: []string{
				structs.NodePoolAll, structs.NodePoolDefault,
				dev1.Name, dev2.Name, prod.Name,
			},
		},
		{
			name:     "other context",
			prefix:   "dev",
			context:  structs.Jobs,
			expected: []string{},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			req := &structs.SearchRequest{
				Prefix:  tc.prefix,
				Context: tc.context,
				QueryOptions: structs.QueryOptions{
					Region: "global",
				},
			}
			var resp structs.SearchResponse
			err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
			must.NoError(t, err)
			must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])

			for k, v := range resp.Matches {
				switch k {
				case structs.NodePools:
					must.SliceContainsAll(t, v, tc.expected)
				default:
					must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
				}
			}
		})
	}
}

func TestSearch_PrefixSearch_NodePool_ACL(t *testing.T) {
	ci.Parallel(t)

	// Start test server with ACL.
	s, root, cleanupS := TestACLServer(t, nil)
	defer cleanupS()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	// Populate state with test node pools and ACL policies.
	fsmState := s.fsm.State()

	dev1 := &structs.NodePool{Name: "dev-1"}
	dev2 := &structs.NodePool{Name: "dev-2"}
	prod := &structs.NodePool{Name: "prod"}
	err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{dev1, dev2, prod})
	must.NoError(t, err)

	devToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1001, "dev-node-pools",
		mock.NodePoolPolicy("dev-*", "read", nil),
	)
	noPolicyToken := mock.CreateToken(t, s.fsm.State(), 1003, nil)
	allPoolsToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1005, "all-node-pools",
		mock.NodePoolPolicy("*", "read", nil),
	)
	denyDevToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1007, "deny-dev-node-pools",
		mock.NodePoolPolicy("dev-*", "deny", nil),
	)

	// Run test cases.
	testCases := []struct {
		name        string
		token       string
		prefix      string
		expected    []string
		expectedErr string
	}{
		{
			name:   "management token has access to all",
			token:  root.SecretID,
			prefix: "",
			expected: []string{
				structs.NodePoolAll, structs.NodePoolDefault,
				dev1.Name, dev2.Name, prod.Name,
			},
		},
		{
			name:   "all pools access",
			token:  allPoolsToken.SecretID,
			prefix: "",
			expected: []string{
				structs.NodePoolAll, structs.NodePoolDefault,
				dev1.Name, dev2.Name, prod.Name,
			},
		},
		{
			name:     "only return what token has access",
			token:    devToken.SecretID,
			prefix:   "dev",
			expected: []string{dev1.Name, dev2.Name},
		},
		{
			name:     "no results if token doesn't have access",
			token:    devToken.SecretID,
			prefix:   "prod",
			expected: []string{},
		},
		{
			name:        "no results if token is denied",
			token:       denyDevToken.SecretID,
			prefix:      "dev",
			expectedErr: structs.ErrPermissionDenied.Error(),
		},
		{
			name:        "no policy",
			token:       noPolicyToken.SecretID,
			prefix:      "",
			expectedErr: structs.ErrPermissionDenied.Error(),
		},
		{
			name:        "no token",
			token:       "",
			prefix:      "",
			expectedErr: structs.ErrPermissionDenied.Error(),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			req := &structs.SearchRequest{
				Prefix:  tc.prefix,
				Context: structs.NodePools,
				QueryOptions: structs.QueryOptions{
					Region:    "global",
					AuthToken: tc.token,
				},
			}
			var resp structs.SearchResponse
			err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
			if tc.expectedErr != "" {
				must.ErrorContains(t, err, tc.expectedErr)
				return
			}

			must.NoError(t, err)
			must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])

			for k, v := range resp.Matches {
				switch k {
				case structs.NodePools:
					must.SliceContainsAll(t, v, tc.expected)
				default:
					must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
				}
			}
		})
	}
}

func TestSearch_PrefixSearch_Deployment(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	deployment := mock.Deployment()
	require.NoError(t, s.fsm.State().UpsertDeployment(2000, deployment))

	prefix := deployment.ID[:len(deployment.ID)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Deployments,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: deployment.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
	require.Len(t, resp.Matches[structs.Deployments], 1)
	require.Equal(t, deployment.ID, resp.Matches[structs.Deployments][0])
	require.False(t, resp.Truncations[structs.Deployments])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_PrefixSearch_AllContext(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	fsmState := s.fsm.State()
	node := mock.Node()

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 100, node))

	eval1 := mock.Eval()
	eval1.ID = node.ID
	require.NoError(t, fsmState.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1}))

	prefix := node.ID[:len(node.ID)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.All,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: eval1.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Nodes], 1)
	require.Len(t, resp.Matches[structs.Evals], 1)
	require.Equal(t, node.ID, resp.Matches[structs.Nodes][0])
	require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0])
	require.Equal(t, uint64(1000), resp.Index)
}

// Tests that the top 20 matches are returned when no prefix is set
func TestSearch_PrefixSearch_NoPrefix(t *testing.T) {
	ci.Parallel(t)

	prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	job := registerMockJob(s, t, prefix, 0)

	req := &structs.SearchRequest{
		Prefix:  "",
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
	require.Len(t, resp.Matches[structs.Jobs], 1)
	require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

// Tests that the zero matches are returned when a prefix has no matching
// results
func TestSearch_PrefixSearch_NoMatches(t *testing.T) {
	ci.Parallel(t)

	prefix := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: structs.DefaultNamespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
	require.Empty(t, resp.Matches[structs.Jobs])
	require.Equal(t, uint64(0), resp.Index)
}

// Prefixes can only be looked up if their length is a power of two. For
// prefixes which are an odd length, use the length-1 characters.
func TestSearch_PrefixSearch_RoundDownToEven(t *testing.T) {
	ci.Parallel(t)

	id1 := "aaafaaaa-e8f7-fd38-c855-ab94ceb89"
	id2 := "aaafeaaa-e8f7-fd38-c855-ab94ceb89"
	prefix := "aaafa"

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	job := registerMockJob(s, t, id1, 0)
	registerMockJob(s, t, id2, 50)

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
	require.Len(t, resp.Matches[structs.Jobs], 1)
	require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
}

func TestSearch_PrefixSearch_MultiRegion(t *testing.T) {
	ci.Parallel(t)

	jobName := "exampleexample"

	s1, cleanupS1 := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
		c.Region = "foo"
	})
	defer cleanupS1()

	s2, cleanupS2 := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
		c.Region = "bar"
	})
	defer cleanupS2()

	TestJoin(t, s1, s2)
	testutil.WaitForLeader(t, s1.RPC)

	job := registerMockJob(s1, t, jobName, 0)

	req := &structs.SearchRequest{
		Prefix:  "",
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "foo",
			Namespace: job.Namespace,
		},
	}

	codec := rpcClient(t, s2)

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Jobs], 1)
	require.Equal(t, job.ID, resp.Matches[structs.Jobs][0])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_PrefixSearch_CSIPlugin(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	id := uuid.Generate()
	state.CreateTestCSIPlugin(s.fsm.State(), id)

	prefix := id[:len(id)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Plugins,
		QueryOptions: structs.QueryOptions{
			Region: "global",
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Plugins], 1)
	require.Equal(t, id, resp.Matches[structs.Plugins][0])
	require.False(t, resp.Truncations[structs.Plugins])
}

func TestSearch_PrefixSearch_CSIVolume(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	id := uuid.Generate()
	err := s.fsm.State().UpsertCSIVolume(1000, []*structs.CSIVolume{{
		ID:        id,
		Namespace: structs.DefaultNamespace,
		PluginID:  "glade",
	}})
	require.NoError(t, err)

	prefix := id[:len(id)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Volumes,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: structs.DefaultNamespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Volumes], 1)
	require.Equal(t, id, resp.Matches[structs.Volumes][0])
	require.False(t, resp.Truncations[structs.Volumes])
}

func TestSearch_PrefixSearch_Namespace(t *testing.T) {
	ci.Parallel(t)

	s, cleanup := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanup()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	ns := mock.Namespace()
	require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))

	prefix := ns.Name[:len(ns.Name)-2]

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.Namespaces,
		QueryOptions: structs.QueryOptions{
			Region: "global",
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

	require.Len(t, resp.Matches[structs.Namespaces], 1)
	require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0])
	require.False(t, resp.Truncations[structs.Namespaces])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_PrefixSearch_Namespace_ACL(t *testing.T) {
	ci.Parallel(t)

	s, root, cleanup := TestACLServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanup()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	store := s.fsm.State()

	ns := mock.Namespace()
	must.NoError(t, store.UpsertNamespaces(500, []*structs.Namespace{ns}))

	job1 := mock.Job()
	must.NoError(t, store.UpsertJob(structs.MsgTypeTestSetup, 502, nil, job1))

	job2 := mock.Job()
	job2.Namespace = ns.Name
	must.NoError(t, store.UpsertJob(structs.MsgTypeTestSetup, 504, nil, job2))

	node := mock.Node()
	must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1001, node))

	req := &structs.SearchRequest{
		Prefix:  "",
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job1.Namespace,
		},
	}

	// Try without a token and expect failure
	{
		var resp structs.SearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with an invalid token and expect failure
	{
		invalidToken := mock.CreatePolicyAndToken(t, store, 1003, "test-invalid",
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
		req.AuthToken = invalidToken.SecretID
		var resp structs.SearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect failure due to Namespaces being the context
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
		req.Context = structs.Namespaces
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect success due to All context
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
		must.Eq(t, uint64(1001), resp.Index)
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])

		// Jobs filtered out since token only has access to node:read
		must.SliceEmpty(t, resp.Matches[structs.Jobs])
	}

	// Try with a valid token for non-default namespace:read-job
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1009, "test-valid2",
			mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob}))
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		req.Namespace = job2.Namespace
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{job2.ID}, resp.Matches[structs.Jobs])
		must.Eq(t, []string{ns.Name}, resp.Matches[structs.Namespaces])

		// Index of job - not node - because node context is filtered out
		must.Eq(t, uint64(504), resp.Index)

		// Nodes filtered out since token only has access to namespace:read-job
		must.SliceEmpty(t, resp.Matches[structs.Nodes])
	}

	// Try with a valid token for node:read and default namespace:read-job
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1011, "test-valid3", strings.Join([]string{
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
			mock.NodePolicy(acl.PolicyRead),
		}, "\n"))
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		req.Namespace = structs.DefaultNamespace
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{job1.ID}, resp.Matches[structs.Jobs])
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
		must.Eq(t, []string{"default"}, resp.Matches[structs.Namespaces])

		must.Eq(t, uint64(1001), resp.Index)

	}

	// Try with a management token
	{
		req.Context = structs.All
		req.AuthToken = root.SecretID
		req.Namespace = structs.DefaultNamespace
		var resp structs.SearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))

		must.Eq(t, []string{job1.ID}, resp.Matches[structs.Jobs])
		must.Eq(t, []string{node.ID}, resp.Matches[structs.Nodes])
		must.Eq(t, []string{"default", ns.Name}, resp.Matches[structs.Namespaces])

		must.Eq(t, uint64(1001), resp.Index)
	}
}

func TestSearch_PrefixSearch_ScalingPolicy(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	job, policy := mock.JobWithScalingPolicy()
	prefix := policy.ID
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, jobIndex, nil, job))

	req := &structs.SearchRequest{
		Prefix:  prefix,
		Context: structs.ScalingPolicies,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.SearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
	require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
	require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0])
	require.Equal(t, uint64(jobIndex), resp.Index)

	req.Context = structs.All
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
	require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
	require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_FuzzySearch_ACL(t *testing.T) {
	ci.Parallel(t)

	s, root, cleanupS := TestACLServer(t, func(c *Config) {
		c.NumSchedulers = 0
		c.SearchConfig.MinTermLength = 1
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	store := s.fsm.State()

	ns := mock.Namespace()
	ns.Name = "not-allowed"
	must.NoError(t, store.UpsertNamespaces(10, []*structs.Namespace{ns}))

	job := mock.Job()
	registerJob(s, t, job)

	variable := mock.VariableEncrypted()
	variable.Path = "test-path/o"
	resp := store.VarSet(1001, &structs.VarApplyStateRequest{
		Op:  structs.VarOpSet,
		Var: variable,
	})
	must.NoError(t, resp.Error)

	plugin := mock.CSIPlugin()
	plugin.ID = "mock.hashicorp.com"
	must.NoError(t, store.UpsertCSIPlugin(1002, plugin))

	node := mock.Node()
	must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1003, node))

	disallowedVariable := mock.VariableEncrypted()
	disallowedVariable.Namespace = "not-allowed"
	resp = store.VarSet(2001, &structs.VarApplyStateRequest{
		Op:  structs.VarOpSet,
		Var: disallowedVariable,
	})
	must.NoError(t, resp.Error)

	req := &structs.FuzzySearchRequest{
		Text:         "set-this-in-test",
		Context:      structs.Jobs,
		QueryOptions: structs.QueryOptions{Region: "global", Namespace: job.Namespace},
	}

	// Try without a token and expect failure
	{
		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with an invalid token and expect failure
	{
		invalidToken := mock.CreatePolicyAndToken(t, store, 1003, "test-invalid",
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
		req.AuthToken = invalidToken.SecretID
		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect failure due to Jobs being the context
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
		req.AuthToken = validToken.SecretID
		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		must.EqError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect success due to All context
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		req.Text = "oo" // mock node ID is foobar
		var resp structs.FuzzySearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

		must.Eq(t, []structs.FuzzyMatch{{ID: node.Name, Scope: []string{node.ID}}},
			resp.Matches[structs.Nodes])

		// Jobs, Plugins, Variables filtered out since token only has access to
		// node:read
		must.SliceEmpty(t, resp.Matches[structs.Jobs])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])
		must.SliceEmpty(t, resp.Matches[structs.Variables])

		must.Eq(t, uint64(1003), resp.Index) // index of node
	}

	// Try with a valid token for namespace:read-job
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1009, "test-valid2",
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
		req.AuthToken = validToken.SecretID
		req.Text = "jo" // mock job Name is my-job
		var resp structs.FuzzySearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Jobs], 1)
		must.Eq(t, structs.FuzzyMatch{
			ID:    "my-job",
			Scope: []string{"default", job.ID},
		}, resp.Matches[structs.Jobs][0])

		// Index of job - not node - because node context is filtered out
		must.Eq(t, uint64(1000), resp.Index)

		// Nodes filtered out since token only has access to namespace:read-job
		must.SliceEmpty(t, resp.Matches[structs.Nodes])
	}

	// Try with a valid token for node:read and namespace:variable:read
	{
		validToken := mock.CreatePolicyAndToken(t, store, 1012, "test-valid4", strings.Join([]string{
			mock.NamespacePolicyWithVariables(structs.DefaultNamespace, "", []string{},
				map[string][]string{"*": []string{"list"}}),
			mock.NodePolicy(acl.PolicyRead),
		}, "\n"))
		req.Text = "o" // matches Job:my-job, Node:foobar, Plugin, and Variables
		req.AuthToken = validToken.SecretID
		var resp structs.FuzzySearchResponse
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

		must.SliceEmpty(t, resp.Matches[structs.Jobs])
		must.SliceEmpty(t, resp.Matches[structs.Plugins])

		must.Eq(t, []structs.FuzzyMatch{
			{ID: node.Name, Scope: []string{node.ID}}},
			resp.Matches[structs.Nodes])

		must.Eq(t, []structs.FuzzyMatch{{
			ID:    variable.Path,
			Scope: []string{structs.DefaultNamespace, variable.Path}}},
			resp.Matches[structs.Variables])

		must.Eq(t, uint64(2001), resp.Index) // index of variables
	}

	// Try with a management token
	{
		req.AuthToken = root.SecretID
		var resp structs.FuzzySearchResponse
		req.Text = "o" // matches Job:my-job, Node:foobar, Plugin, and Variables
		must.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

		must.Eq(t, []structs.FuzzyMatch{
			{ID: job.Name, Scope: []string{"default", job.ID}}},
			resp.Matches[structs.Jobs])

		must.Eq(t, []structs.FuzzyMatch{
			{ID: node.Name, Scope: []string{node.ID}}},
			resp.Matches[structs.Nodes])

		must.Eq(t, []structs.FuzzyMatch{{ID: plugin.ID}},
			resp.Matches[structs.Plugins])

		must.Eq(t, []structs.FuzzyMatch{{
			ID:    variable.Path,
			Scope: []string{structs.DefaultNamespace, variable.Path}}},
			resp.Matches[structs.Variables])

		must.Eq(t, uint64(2001), resp.Index) // index of variables
	}
}

func TestSearch_FuzzySearch_NotEnabled(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
		c.SearchConfig.FuzzyEnabled = false
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	job := mock.Job()
	registerJob(s, t, job)

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))

	req := &structs.FuzzySearchRequest{
		Text:         "foo", // min set to 5
		Context:      structs.Jobs,
		QueryOptions: structs.QueryOptions{Region: "global", Namespace: job.Namespace},
	}

	var resp structs.FuzzySearchResponse
	require.EqualError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp),
		"fuzzy search is not enabled")
}

func TestSearch_FuzzySearch_ShortText(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
		c.SearchConfig.MinTermLength = 5
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	job := mock.Job()
	registerJob(s, t, job)

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))

	req := &structs.FuzzySearchRequest{
		Text:         "foo", // min set to 5
		Context:      structs.Jobs,
		QueryOptions: structs.QueryOptions{Region: "global", Namespace: job.Namespace},
	}

	var resp structs.FuzzySearchResponse
	require.EqualError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp),
		"fuzzy search query must be at least 5 characters, got 3")
}

func TestSearch_FuzzySearch_TruncateLimitQuery(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))

	req := &structs.FuzzySearchRequest{
		Text:         "job",
		Context:      structs.Jobs,
		QueryOptions: structs.QueryOptions{Region: "global", Namespace: "default"},
	}

	for i := 0; i < 25; i++ {
		job := mock.Job()
		job.Name = fmt.Sprintf("my-job-%d", i)
		registerJob(s, t, job)
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Jobs], 20)
	require.True(t, resp.Truncations[structs.Jobs])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_FuzzySearch_TruncateLimitResults(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
		c.SearchConfig.LimitQuery = 10000
		c.SearchConfig.LimitResults = 5
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, mock.Node()))

	req := &structs.FuzzySearchRequest{
		Text:         "job",
		Context:      structs.Jobs,
		QueryOptions: structs.QueryOptions{Region: "global", Namespace: "default"},
	}

	for i := 0; i < 25; i++ {
		job := mock.Job()
		job.Name = fmt.Sprintf("my-job-%d", i)
		registerJob(s, t, job)
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Jobs], 5)
	require.True(t, resp.Truncations[structs.Jobs])
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_FuzzySearch_Evals(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	eval1 := mock.Eval()
	eval1.ID = "f7dee5a1-d2b0-2f6a-2e75-6c8e467a4b99"
	require.NoError(t, s.fsm.State().UpsertEvals(structs.MsgTypeTestSetup, 2000, []*structs.Evaluation{eval1}))

	req := &structs.FuzzySearchRequest{
		Text:    "f7dee", // evals are prefix searched
		Context: structs.Evals,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: eval1.Namespace,
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Evals], 1)
	require.Equal(t, eval1.ID, resp.Matches[structs.Evals][0].ID)
	require.False(t, resp.Truncations[structs.Evals])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_FuzzySearch_Allocation(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	alloc := mockAlloc()
	summary := mock.JobSummary(alloc.JobID)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJobSummary(999, summary))
	require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, 90, []*structs.Allocation{alloc}))

	req := &structs.FuzzySearchRequest{
		Text:    "web",
		Context: structs.Allocs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: alloc.Namespace,
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Allocs], 1)
	require.Equal(t, alloc.Name, resp.Matches[structs.Allocs][0].ID)
	require.False(t, resp.Truncations[structs.Allocs])
	require.Equal(t, uint64(90), resp.Index)
}

func TestSearch_FuzzySearch_Node(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	fsmState := s.fsm.State()
	node := mock.Node()

	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 100, node))

	req := &structs.FuzzySearchRequest{
		Text:    "oo",
		Context: structs.Nodes,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: structs.DefaultNamespace,
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
	require.Len(t, resp.Matches[structs.Nodes], 1)
	require.Equal(t, node.Name, resp.Matches[structs.Nodes][0].ID)
	require.False(t, resp.Truncations[structs.Nodes])
	require.Equal(t, uint64(100), resp.Index)
}

func TestSearch_FuzzySearch_NodePool(t *testing.T) {
	ci.Parallel(t)

	// Start test server.
	s, cleanupS := TestServer(t, nil)
	defer cleanupS()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	// Populate state with test node pools.
	fsmState := s.fsm.State()
	devEng := &structs.NodePool{Name: "dev-eng"}
	devInfra := &structs.NodePool{Name: "dev-infra"}
	prodEng := &structs.NodePool{Name: "prod-eng"}

	err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{devEng, devInfra, prodEng})
	must.NoError(t, err)

	// Run test cases.
	testCases := []struct {
		name        string
		text        string
		context     structs.Context
		expected    []string
		expectedErr string
	}{
		{
			name:     "fuzzy match",
			text:     "eng",
			context:  structs.NodePools,
			expected: []string{devEng.Name, prodEng.Name},
		},
		{
			name:     "fuzzy match - all",
			text:     "eng",
			context:  structs.All,
			expected: []string{devEng.Name, prodEng.Name},
		},
		{
			name:        "empty prefix",
			text:        "",
			context:     structs.NodePools,
			expectedErr: "search query must be at least 2 characters",
		},
		{
			name:     "other context",
			text:     "eng",
			context:  structs.Jobs,
			expected: []string{},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			req := &structs.FuzzySearchRequest{
				Text:    tc.text,
				Context: tc.context,
				QueryOptions: structs.QueryOptions{
					Region: "global",
				},
			}
			var resp structs.FuzzySearchResponse
			err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
			if tc.expectedErr != "" {
				must.ErrorContains(t, err, tc.expectedErr)
				return
			}
			must.NoError(t, err)
			must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])

			for k, v := range resp.Matches {
				switch k {
				case structs.NodePools:
					got := make([]string, len(v))
					for i, m := range v {
						got[i] = m.ID
					}
					must.SliceContainsAll(t, got, tc.expected)
				default:
					must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
				}
			}
		})
	}
}

func TestSearch_FuzzySearch_NodePool_ACL(t *testing.T) {
	ci.Parallel(t)

	// Start test server with ACL.
	s, root, cleanupS := TestACLServer(t, nil)
	defer cleanupS()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	// Populate state with test node pools and ACL policies.
	fsmState := s.fsm.State()

	devEng := &structs.NodePool{Name: "dev-eng"}
	devInfra := &structs.NodePool{Name: "dev-infra"}
	prodEng := &structs.NodePool{Name: "prod-eng"}

	err := fsmState.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{devEng, devInfra, prodEng})
	must.NoError(t, err)

	engToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1001, "eng-node-pools",
		mock.NodePoolPolicy("*eng", "read", nil),
	)
	noPolicyToken := mock.CreateToken(t, s.fsm.State(), 1003, nil)
	allPoolsToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1005, "all-node-pools",
		mock.NodePoolPolicy("*", "read", nil),
	)
	denyEngToken := mock.CreatePolicyAndToken(t, s.fsm.State(), 1007, "deny-eng-node-pools",
		mock.NodePoolPolicy("*eng", "deny", nil),
	)

	// Run test cases.
	testCases := []struct {
		name        string
		token       string
		text        string
		expected    []string
		expectedErr string
	}{
		{
			name:     "management token has access to all",
			token:    root.SecretID,
			text:     "dev",
			expected: []string{devEng.Name, devInfra.Name},
		},
		{
			name:     "all pools access",
			token:    allPoolsToken.SecretID,
			text:     "dev",
			expected: []string{devEng.Name, devInfra.Name},
		},
		{
			name:     "only return what token has access",
			token:    engToken.SecretID,
			text:     "eng",
			expected: []string{devEng.Name, prodEng.Name},
		},
		{
			name:     "no results if token doesn't have access",
			token:    engToken.SecretID,
			text:     "infra",
			expected: []string{},
		},
		{
			name:        "no results if token is denied",
			token:       denyEngToken.SecretID,
			text:        "eng",
			expectedErr: structs.ErrPermissionDenied.Error(),
		},
		{
			name:        "no policy",
			token:       noPolicyToken.SecretID,
			text:        "dev",
			expectedErr: structs.ErrPermissionDenied.Error(),
		},
		{
			name:        "no token",
			token:       "",
			text:        "dev",
			expectedErr: structs.ErrPermissionDenied.Error(),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			req := &structs.FuzzySearchRequest{
				Text:    tc.text,
				Context: structs.NodePools,
				QueryOptions: structs.QueryOptions{
					Region:    "global",
					AuthToken: tc.token,
				},
			}
			var resp structs.FuzzySearchResponse
			err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
			if tc.expectedErr != "" {
				must.ErrorContains(t, err, tc.expectedErr)
				return
			}

			must.NoError(t, err)
			must.Len(t, len(tc.expected), resp.Matches[structs.NodePools])

			for k, v := range resp.Matches {
				switch k {
				case structs.NodePools:
					got := make([]string, len(v))
					for i, m := range v {
						got[i] = m.ID
					}
					must.SliceContainsAll(t, got, tc.expected)
				default:
					must.Len(t, 0, v, must.Sprintf("found %d results in %v: %v", len(v), k, v))
				}
			}
		})
	}
}

func TestSearch_FuzzySearch_Deployment(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	deployment := mock.Deployment()
	require.NoError(t, s.fsm.State().UpsertDeployment(2000, deployment))

	req := &structs.FuzzySearchRequest{
		Text:    deployment.ID[0:3], // deployments are prefix searched
		Context: structs.Deployments,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: deployment.Namespace,
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
	require.Len(t, resp.Matches[structs.Deployments], 1)
	require.Equal(t, deployment.ID, resp.Matches[structs.Deployments][0].ID)
	require.False(t, resp.Truncations[structs.Deployments])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_FuzzySearch_CSIPlugin(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	state.CreateTestCSIPlugin(s.fsm.State(), "my-plugin")

	req := &structs.FuzzySearchRequest{
		Text:    "lug",
		Context: structs.Plugins,
		QueryOptions: structs.QueryOptions{
			Region: "global",
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Plugins], 1)
	require.Equal(t, "my-plugin", resp.Matches[structs.Plugins][0].ID)
	require.False(t, resp.Truncations[structs.Plugins])
}

func TestSearch_FuzzySearch_CSIVolume(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	id := uuid.Generate()
	err := s.fsm.State().UpsertCSIVolume(1000, []*structs.CSIVolume{{
		ID:        id,
		Namespace: structs.DefaultNamespace,
		PluginID:  "glade",
	}})
	require.NoError(t, err)

	req := &structs.FuzzySearchRequest{
		Text:    id[0:3], // volumes are prefix searched
		Context: structs.Volumes,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: structs.DefaultNamespace,
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Volumes], 1)
	require.Equal(t, id, resp.Matches[structs.Volumes][0].ID)
	require.False(t, resp.Truncations[structs.Volumes])
}

func TestSearch_FuzzySearch_Namespace(t *testing.T) {
	ci.Parallel(t)

	s, cleanup := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanup()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	ns := mock.Namespace()
	require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))

	req := &structs.FuzzySearchRequest{
		Text:    "am", // mock is team-<uuid>
		Context: structs.Namespaces,
		QueryOptions: structs.QueryOptions{
			Region: "global",
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Namespaces], 1)
	require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0].ID)
	require.False(t, resp.Truncations[structs.Namespaces])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_FuzzySearch_Namespace_caseInsensitive(t *testing.T) {
	ci.Parallel(t)

	s, cleanup := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanup()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	ns := mock.Namespace()
	ns.Name = "TheFooNamespace"
	require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))

	req := &structs.FuzzySearchRequest{
		Text:    "foon",
		Context: structs.Namespaces,
		QueryOptions: structs.QueryOptions{
			Region: "global",
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

	require.Len(t, resp.Matches[structs.Namespaces], 1)
	require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0].ID)
	require.False(t, resp.Truncations[structs.Namespaces])
	require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_FuzzySearch_ScalingPolicy(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)

	job, policy := mock.JobWithScalingPolicy()
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, jobIndex, nil, job))

	req := &structs.FuzzySearchRequest{
		Text:    policy.ID[0:3], // scaling policies are prefix searched
		Context: structs.ScalingPolicies,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job.Namespace,
		},
	}

	var resp structs.FuzzySearchResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
	require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
	require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0].ID)
	require.Equal(t, uint64(jobIndex), resp.Index)

	req.Context = structs.All
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
	require.Len(t, resp.Matches[structs.ScalingPolicies], 1)
	require.Equal(t, policy.ID, resp.Matches[structs.ScalingPolicies][0].ID)
	require.Equal(t, uint64(jobIndex), resp.Index)
}

func TestSearch_FuzzySearch_Namespace_ACL(t *testing.T) {
	ci.Parallel(t)

	s, root, cleanup := TestACLServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanup()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	ns := mock.Namespace()
	ns.Name = "team-job-app"
	require.NoError(t, fsmState.UpsertNamespaces(500, []*structs.Namespace{ns}))

	job1 := mock.Job()
	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, 502, nil, job1))

	job2 := mock.Job()
	job2.Namespace = ns.Name
	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, 504, nil, job2))

	node := mock.Node()
	node.Name = "run-jobs"
	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1001, node))

	req := &structs.FuzzySearchRequest{
		Text:    "set-text-in-test",
		Context: structs.Jobs,
		QueryOptions: structs.QueryOptions{
			Region:    "global",
			Namespace: job1.Namespace,
		},
	}

	// Try without a token and expect failure
	{
		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		require.EqualError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with an invalid token and expect failure
	{
		invalidToken := mock.CreatePolicyAndToken(t, fsmState, 1003, "test-invalid",
			mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
		req.AuthToken = invalidToken.SecretID
		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		require.EqualError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect failure due to Namespaces being the context
	{
		validToken := mock.CreatePolicyAndToken(t, fsmState, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
		req.Context = structs.Namespaces
		req.AuthToken = validToken.SecretID
		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		require.EqualError(t, err, structs.ErrPermissionDenied.Error())
	}

	// Try with a node:read token and expect success due to All context
	{
		validToken := mock.CreatePolicyAndToken(t, fsmState, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
		req.Text = "job"
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Equal(t, uint64(1001), resp.Index)
		require.Len(t, resp.Matches[structs.Nodes], 1)

		// Jobs filtered out since token only has access to node:read
		require.Len(t, resp.Matches[structs.Jobs], 0)
	}

	// Try with a valid token for non-default namespace:read-job
	{
		validToken := mock.CreatePolicyAndToken(t, fsmState, 1009, "test-valid2",
			mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob}))
		req.Text = "job"
		req.Context = structs.All
		req.AuthToken = validToken.SecretID
		req.Namespace = job2.Namespace
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Jobs], 1)
		require.Equal(t, job2.Name, resp.Matches[structs.Jobs][0].ID)

		// Index of job - not node - because node context is filtered out
		require.Equal(t, uint64(504), resp.Index)

		// Nodes filtered out since token only has access to namespace:read-job
		require.Len(t, resp.Matches[structs.Nodes], 0)
	}

	// Try with a management token
	{
		req.Text = "job"
		req.Context = structs.All
		req.AuthToken = root.SecretID
		req.Namespace = job1.Namespace
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Equal(t, uint64(1001), resp.Index)
		require.Len(t, resp.Matches[structs.Jobs], 1)
		require.Equal(t, job1.Name, resp.Matches[structs.Jobs][0].ID)
		require.Len(t, resp.Matches[structs.Nodes], 1)
		require.Len(t, resp.Matches[structs.Namespaces], 1) // matches "team-job-app"
	}
}

func TestSearch_FuzzySearch_MultiNamespace_ACL(t *testing.T) {
	ci.Parallel(t)

	s, root, cleanupS := TestACLServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()

	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	require.NoError(t, fsmState.UpsertNamespaces(500, []*structs.Namespace{{
		Name:        "teamA",
		Description: "first namespace",
		CreateIndex: 100,
		ModifyIndex: 200,
	}, {
		Name:        "teamB",
		Description: "second namespace",
		CreateIndex: 101,
		ModifyIndex: 201,
	}, {
		Name:        "teamC",
		Description: "third namespace",
		CreateIndex: 102,
		ModifyIndex: 202,
	}}))

	// Closure to simplify fsm indexing
	index := uint64(1000)
	inc := func() uint64 {
		index++
		return index
	}

	// Upsert 3 jobs each in separate namespace
	job1 := mock.Job()
	job1.Name = "teamA-job1"
	job1.ID = "job1"
	job1.Namespace = "teamA"
	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, inc(), nil, job1))

	job2 := mock.Job()
	job2.Name = "teamB-job2"
	job2.ID = "job2"
	job2.Namespace = "teamB"
	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, inc(), nil, job2))

	job3 := mock.Job()
	job3.Name = "teamC-job3"
	job3.ID = "job3"
	job3.Namespace = "teamC"
	require.NoError(t, fsmState.UpsertJob(structs.MsgTypeTestSetup, inc(), nil, job3))

	// Upsert a node
	node := mock.Node()
	node.Name = "node-for-teams"
	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, inc(), node))

	// Upsert a node that will not be matched
	node2 := mock.Node()
	node2.Name = "node-for-ops"
	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, inc(), node2))

	// Create parameterized requests
	request := func(text, namespace, token string, context structs.Context) *structs.FuzzySearchRequest {
		return &structs.FuzzySearchRequest{
			Text:    text,
			Context: context,
			QueryOptions: structs.QueryOptions{
				Region:    "global",
				Namespace: namespace,
				AuthToken: token,
			},
		}
	}

	t.Run("without a token expect failure", func(t *testing.T) {
		var resp structs.FuzzySearchResponse
		req := request("anything", job1.Namespace, "", structs.Jobs)
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		require.EqualError(t, err, structs.ErrPermissionDenied.Error())
	})

	t.Run("with an invalid token expect failure", func(t *testing.T) {
		policy := mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs})
		invalidToken := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-invalid", policy)
		req := request("anything", job1.Namespace, invalidToken.SecretID, structs.Jobs)

		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		require.EqualError(t, err, structs.ErrPermissionDenied.Error())
	})

	t.Run("with node:read token search namespaces expect failure", func(t *testing.T) {
		validToken := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-invalid2", mock.NodePolicy(acl.PolicyRead))
		req := request("team", job1.Namespace, validToken.SecretID, structs.Namespaces)

		var resp structs.FuzzySearchResponse
		err := msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp)
		require.EqualError(t, err, structs.ErrPermissionDenied.Error())
	})

	t.Run("with node:read token search all expect success", func(t *testing.T) {
		validToken := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-valid", mock.NodePolicy(acl.PolicyRead))
		req := request("team", job1.Namespace, validToken.SecretID, structs.All)

		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

		// One matching node
		require.Len(t, resp.Matches[structs.Nodes], 1)

		// Jobs filtered out since token only has access to node:read
		require.Len(t, resp.Matches[structs.Jobs], 0)
	})

	t.Run("with a teamB/job:read token search all expect 1 job", func(t *testing.T) {
		policy := mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
		token := mock.CreatePolicyAndToken(t, fsmState, inc(), "test-valid2", policy)
		req := request("team", job2.Namespace, token.SecretID, structs.All)

		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Jobs], 1)
		require.Equal(t, job2.Name, resp.Matches[structs.Jobs][0].ID)

		// Nodes filtered out since token only has access to namespace:read-job
		require.Len(t, resp.Matches[structs.Nodes], 0)
	})

	// Using a token that can read jobs in 2 namespaces, we should get job results from
	// both those namespaces (using wildcard namespace in the query) but not the
	// third (and from no other contexts).
	t.Run("with a multi-ns job:read token search all expect 2 jobs", func(t *testing.T) {
		policyB := mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
		mock.CreatePolicy(t, fsmState, inc(), "policyB", policyB)

		policyC := mock.NamespacePolicy(job3.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
		mock.CreatePolicy(t, fsmState, inc(), "policyC", policyC)

		token := mock.CreateToken(t, fsmState, inc(), []string{"policyB", "policyC"})
		req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Jobs)

		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Jobs], 2)
		require.Equal(t, job2.Name, resp.Matches[structs.Jobs][0].ID)
		require.Equal(t, job3.Name, resp.Matches[structs.Jobs][1].ID)
	})

	// Using a management token, we should get job results from all three namespaces
	// (using wildcard namespace in the query).
	t.Run("with a management token search all expect 3 jobs", func(t *testing.T) {
		req := request("team", structs.AllNamespacesSentinel, root.SecretID, structs.Jobs)

		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Jobs], 3)
		require.Equal(t, job1.Name, resp.Matches[structs.Jobs][0].ID)
		require.Equal(t, job2.Name, resp.Matches[structs.Jobs][1].ID)
		require.Equal(t, job3.Name, resp.Matches[structs.Jobs][2].ID)
	})

	// Using a token that can read nodes, we should get our 1 matching node when
	// searching the nodes context.
	t.Run("with node:read token read nodes", func(t *testing.T) {
		policy := mock.NodePolicy("read")
		mock.CreatePolicy(t, fsmState, inc(), "node-read-policy", policy)

		token := mock.CreateToken(t, fsmState, inc(), []string{"node-read-policy"})
		req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Nodes)

		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Nodes], 1)
		require.Equal(t, "node-for-teams", resp.Matches[structs.Nodes][0].ID)
	})

	// Using a token that cannot read nodes, we should get no matching nodes when
	// searching the nodes context.
	t.Run("with a job:read token read nodes", func(t *testing.T) {
		policy := mock.AgentPolicy("read")
		mock.CreatePolicy(t, fsmState, inc(), "agent-read-policy", policy)

		token := mock.CreateToken(t, fsmState, inc(), []string{"agent-read-policy"})
		req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Nodes)
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Empty(t, resp.Matches[structs.Nodes])
	})

	// Using a token that can read only job:read one namespace, but with wildcard
	// namespace should return only the one alloc the token can access.
	t.Run("with job:read token read allocs", func(t *testing.T) {
		policyD := mock.NamespacePolicy(job2.Namespace, "", []string{acl.NamespaceCapabilityReadJob})
		mock.CreatePolicy(t, fsmState, inc(), "policyD", policyD)

		// Create an alloc for each of the 3 jobs
		alloc1 := mockAlloc()
		alloc1.JobID = job1.ID
		alloc1.Name = job1.Name + ".task[0]"
		alloc1.Namespace = job1.Namespace
		summary1 := mock.JobSummary(alloc1.JobID)
		require.NoError(t, fsmState.UpsertJobSummary(inc(), summary1))

		alloc2 := mockAlloc()
		alloc2.JobID = job2.ID
		alloc2.Name = job2.Name + ".task[0]"
		alloc2.Namespace = job2.Namespace
		summary2 := mock.JobSummary(alloc2.JobID)
		require.NoError(t, fsmState.UpsertJobSummary(inc(), summary2))

		alloc3 := mockAlloc()
		alloc3.JobID = job3.ID
		alloc3.Name = job3.Name + ".task[0]"
		alloc3.Namespace = job3.Namespace
		summary3 := mock.JobSummary(alloc3.JobID)
		require.NoError(t, fsmState.UpsertJobSummary(inc(), summary3))

		// Upsert the allocs
		require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, inc(), []*structs.Allocation{alloc1, alloc2, alloc3}))

		token := mock.CreateToken(t, fsmState, inc(), []string{"policyD"})
		req := request("team", structs.AllNamespacesSentinel, token.SecretID, structs.Allocs)
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Allocs], 1)
		require.Equal(t, "teamB-job2.task[0]", resp.Matches[structs.Allocs][0].ID)
	})

	// Using a management token should return allocs from all the jobs.
	t.Run("with job:read token read allocs", func(t *testing.T) {
		// Create an alloc for each of the 3 jobs
		alloc1 := mockAlloc()
		alloc1.ID = uuid.Generate()
		alloc1.JobID = job1.ID
		alloc1.Name = "test-alloc.one[0]"
		alloc1.Namespace = job1.Namespace
		summary1 := mock.JobSummary(alloc1.JobID)
		require.NoError(t, fsmState.UpsertJobSummary(inc(), summary1))

		alloc2 := mockAlloc()
		alloc2.ID = uuid.Generate()
		alloc2.JobID = job2.ID
		alloc2.Name = "test-alloc.two[0]"
		alloc2.Namespace = job2.Namespace
		summary2 := mock.JobSummary(alloc2.JobID)
		require.NoError(t, fsmState.UpsertJobSummary(inc(), summary2))

		alloc3 := mockAlloc()
		alloc3.ID = uuid.Generate()
		alloc3.JobID = job3.ID
		alloc3.Name = "test-alloc.three[0]"
		alloc3.Namespace = job3.Namespace
		summary3 := mock.JobSummary(alloc3.JobID)
		require.NoError(t, fsmState.UpsertJobSummary(inc(), summary3))

		// Upsert the allocs
		require.NoError(t, fsmState.UpsertAllocs(structs.MsgTypeTestSetup, inc(), []*structs.Allocation{alloc1, alloc2, alloc3}))

		req := request("alloc", structs.AllNamespacesSentinel, root.SecretID, structs.Allocs)
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		require.Len(t, resp.Matches[structs.Allocs], 3)
		require.Equal(t, alloc1.Name, resp.Matches[structs.Allocs][0].ID)
		require.Equal(t, []string{"teamA", alloc1.ID}, resp.Matches[structs.Allocs][0].Scope)
		require.Equal(t, alloc2.Name, resp.Matches[structs.Allocs][1].ID)
		require.Equal(t, []string{"teamB", alloc2.ID}, resp.Matches[structs.Allocs][1].Scope)
		require.Equal(t, alloc3.Name, resp.Matches[structs.Allocs][2].ID)
		require.Equal(t, []string{"teamC", alloc3.ID}, resp.Matches[structs.Allocs][2].Scope)
	})

	// Allow plugin read and wildcard namespace
	t.Run("with plugin:read", func(t *testing.T) {
		policy := mock.PluginPolicy("read")
		mock.CreatePolicy(t, fsmState, inc(), "plugin-read-policy", policy)

		token := mock.CreateToken(t, fsmState, inc(), []string{"plugin-read-policy"})

		// Create CSI plugins
		state.CreateTestCSIPlugin(s.fsm.State(), "plugin-for-teams")
		state.CreateTestCSIPlugin(s.fsm.State(), "plugin-for-ops")

		req := request("teams", structs.AllNamespacesSentinel, token.SecretID, structs.Plugins)
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

		require.Len(t, resp.Matches[structs.Plugins], 1)
		require.Empty(t, resp.Matches[structs.Plugins][0].Scope) // no scope
	})
}

func TestSearch_FuzzySearch_Job(t *testing.T) {
	ci.Parallel(t)

	s, cleanupS := TestServer(t, func(c *Config) {
		c.NumSchedulers = 0
	})
	defer cleanupS()
	codec := rpcClient(t, s)
	testutil.WaitForLeader(t, s.RPC)
	fsmState := s.fsm.State()

	job := mock.Job()
	job.Name = "demo-sleep"
	job.Namespace = "team-sleepy"
	job.TaskGroups = []*structs.TaskGroup{{
		Name: "qa-sleeper-group-one",
		Services: []*structs.Service{{
			Name: "qa-group-sleep-svc-one",
		}},
		Tasks: []*structs.Task{{
			Name: "qa-sleep-task-one",
			Services: []*structs.Service{{
				Name: "some-sleepy-task-svc-one",
			}},
			Driver: "docker",
			Config: map[string]interface{}{
				"image": "sleeper:latest",
			},
		}},
	}, {
		Name: "prod-sleeper-group-one",
		Tasks: []*structs.Task{{
			Name:   "prod-sleep-task-one",
			Driver: "exec",
			Config: map[string]interface{}{
				"command": "/bin/sleep",
			},
		}, {
			Name:   "prod-task-two",
			Driver: "raw_exec",
			Config: map[string]interface{}{
				"command": "/usr/sbin/sleep",
			},
			Services: []*structs.Service{{
				Name: "some-sleepy-task-svc-two",
			}},
		}},
	}, {
		Name: "sleep-in-java",
		Tasks: []*structs.Task{{
			Name:   "prod-java-sleep",
			Driver: "java",
			Config: map[string]interface{}{
				"class": "sleep.class",
			},
		}},
	}}

	ns := mock.Namespace()
	ns.Name = job.Namespace
	require.NoError(t, fsmState.UpsertNamespaces(2000, []*structs.Namespace{ns}))
	registerJob(s, t, job)
	require.NoError(t, fsmState.UpsertNode(structs.MsgTypeTestSetup, 1003, mock.Node()))

	t.Run("sleep", func(t *testing.T) {
		req := &structs.FuzzySearchRequest{
			Text:    "sleep",
			Context: structs.Jobs,
			QueryOptions: structs.QueryOptions{
				Region:    "global",
				Namespace: job.Namespace,
			},
		}
		var resp structs.FuzzySearchResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))
		m := resp.Matches
		require.Equal(t, uint64(1000), resp.Index) // job is explicit search context, has id=1000

		// just the one job
		require.Len(t, m[structs.Jobs], 1)

		// 3 services (1 group, 2 task)
		require.Len(t, m[structs.Services], 3)
		require.Equal(t, []structs.FuzzyMatch{{
			ID:    "some-sleepy-task-svc-one",
			Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one", "qa-sleep-task-one"},
		}, {
			ID:    "some-sleepy-task-svc-two",
			Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one", "prod-task-two"},
		}, {
			ID:    "qa-group-sleep-svc-one",
			Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one"},
		}}, m[structs.Services])

		// 3 groups
		require.Len(t, m[structs.Groups], 3)
		require.Equal(t, []structs.FuzzyMatch{{
			ID:    "sleep-in-java",
			Scope: []string{"team-sleepy", job.ID},
		}, {
			ID:    "qa-sleeper-group-one",
			Scope: []string{"team-sleepy", job.ID},
		}, {
			ID:    "prod-sleeper-group-one",
			Scope: []string{"team-sleepy", job.ID},
		}}, m[structs.Groups])

		// 3 tasks (1 does not match)
		require.Len(t, m[structs.Tasks], 3)
		require.Equal(t, []structs.FuzzyMatch{{
			ID:    "qa-sleep-task-one",
			Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one"},
		}, {
			ID:    "prod-sleep-task-one",
			Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one"},
		}, {
			ID:    "prod-java-sleep",
			Scope: []string{"team-sleepy", job.ID, "sleep-in-java"},
		}}, m[structs.Tasks])

		// 2 tasks with command
		require.Len(t, m[structs.Commands], 2)
		require.Equal(t, []structs.FuzzyMatch{{
			ID:    "/bin/sleep",
			Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one", "prod-sleep-task-one"},
		}, {
			ID:    "/usr/sbin/sleep",
			Scope: []string{"team-sleepy", job.ID, "prod-sleeper-group-one", "prod-task-two"},
		}}, m[structs.Commands])

		// 1 task with image
		require.Len(t, m[structs.Images], 1)
		require.Equal(t, []structs.FuzzyMatch{{
			ID:    "sleeper:latest",
			Scope: []string{"team-sleepy", job.ID, "qa-sleeper-group-one", "qa-sleep-task-one"},
		}}, m[structs.Images])

		// 1 task with class
		require.Len(t, m[structs.Classes], 1)
		require.Equal(t, []structs.FuzzyMatch{{
			ID:    "sleep.class",
			Scope: []string{"team-sleepy", job.ID, "sleep-in-java", "prod-java-sleep"},
		}}, m[structs.Classes])
	})
}

func TestSearch_FuzzySearch_fuzzyIndex(t *testing.T) {
	ci.Parallel(t)

	for _, tc := range []struct {
		name, text string
		exp        int
	}{
		{name: "foo-bar-baz", text: "bar", exp: 4},
		{name: "Foo-Bar-Baz", text: "bar", exp: 4},
		{name: "foo-bar-baz", text: "zap", exp: -1},
	} {
		result := fuzzyIndex(tc.name, tc.text)
		require.Equal(t, tc.exp, result, "name: %s, text: %s, exp: %d, got: %d", tc.name, tc.text, tc.exp, result)
	}
}
