// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package structs

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	fuzz "github.com/google/gofuzz"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/cache"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/lib"
	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/types"
)

func TestEncodeDecode(t *testing.T) {
	arg := &RegisterRequest{
		Datacenter: "foo",
		Node:       "bar",
		Address:    "baz",
		Service: &NodeService{
			Service: "test",
			Address: "127.0.0.2",
		},
	}
	buf, err := Encode(RegisterRequestType, arg)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	var out RegisterRequest
	err = Decode(buf[1:], &out)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if !reflect.DeepEqual(arg.Service, out.Service) {
		t.Fatalf("bad: %#v %#v", arg.Service, out.Service)
	}
	if !reflect.DeepEqual(arg, &out) {
		t.Fatalf("bad: %#v %#v", arg, out)
	}
}

func TestStructs_Implements(t *testing.T) {
	var (
		_ RPCInfo          = &RegisterRequest{}
		_ RPCInfo          = &DeregisterRequest{}
		_ RPCInfo          = &DCSpecificRequest{}
		_ RPCInfo          = &ServiceSpecificRequest{}
		_ RPCInfo          = &NodeSpecificRequest{}
		_ RPCInfo          = &ChecksInStateRequest{}
		_ RPCInfo          = &KVSRequest{}
		_ RPCInfo          = &KeyRequest{}
		_ RPCInfo          = &KeyListRequest{}
		_ RPCInfo          = &SessionRequest{}
		_ RPCInfo          = &SessionSpecificRequest{}
		_ RPCInfo          = &EventFireRequest{}
		_ RPCInfo          = &ACLPolicyBatchGetRequest{}
		_ RPCInfo          = &ACLPolicyGetRequest{}
		_ RPCInfo          = &ACLTokenGetRequest{}
		_ RPCInfo          = &KeyringRequest{}
		_ CompoundResponse = &KeyringResponses{}
	)
}

func TestStructs_RegisterRequest_ChangesNode(t *testing.T) {

	node := &Node{
		ID:              types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5"),
		Node:            "test",
		Address:         "127.0.0.1",
		Datacenter:      "dc1",
		TaggedAddresses: make(map[string]string),
		Meta: map[string]string{
			"role": "server",
		},
	}

	type testcase struct {
		name   string
		setup  func(*RegisterRequest)
		expect bool
	}

	cases := []testcase{
		{
			name: "id",
			setup: func(r *RegisterRequest) {
				r.ID = "nope"
			},
			expect: true,
		},
		{
			name: "name",
			setup: func(r *RegisterRequest) {
				r.Node = "nope"
			},
			expect: true,
		},
		{
			name: "name casing",
			setup: func(r *RegisterRequest) {
				r.Node = "TeSt"
			},
			expect: false,
		},
		{
			name: "address",
			setup: func(r *RegisterRequest) {
				r.Address = "127.0.0.2"
			},
			expect: true,
		},
		{
			name: "dc",
			setup: func(r *RegisterRequest) {
				r.Datacenter = "dc2"
			},
			expect: true,
		},
		{
			name: "tagged addresses",
			setup: func(r *RegisterRequest) {
				r.TaggedAddresses["wan"] = "nope"
			},
			expect: true,
		},
		{
			name: "node meta",
			setup: func(r *RegisterRequest) {
				r.NodeMeta["invalid"] = "nope"
			},
			expect: true,
		},
	}

	run := func(t *testing.T, tc testcase) {
		req := &RegisterRequest{
			ID:              types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5"),
			Node:            "test",
			Address:         "127.0.0.1",
			Datacenter:      "dc1",
			TaggedAddresses: make(map[string]string),
			NodeMeta: map[string]string{
				"role": "server",
			},
		}

		if req.ChangesNode(node) {
			t.Fatalf("should not change")
		}

		tc.setup(req)

		if tc.expect {
			if !req.ChangesNode(node) {
				t.Fatalf("should change")
			}
		} else {
			if req.ChangesNode(node) {
				t.Fatalf("should not change")
			}
		}

		t.Run("skip node update", func(t *testing.T) {
			req.SkipNodeUpdate = true
			if req.ChangesNode(node) {
				t.Fatalf("should skip")
			}
		})
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

// testServiceNode gives a fully filled out ServiceNode instance.
func testServiceNode(t *testing.T) *ServiceNode {
	return &ServiceNode{
		ID:         types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5"),
		Node:       "node1",
		Address:    "127.0.0.1",
		Datacenter: "dc1",
		TaggedAddresses: map[string]string{
			"hello": "world",
		},
		NodeMeta: map[string]string{
			"tag": "value",
		},
		ServiceKind:    ServiceKindTypical,
		ServiceID:      "service1",
		ServiceName:    "dogs",
		ServiceTags:    []string{"prod", "v1"},
		ServiceAddress: "127.0.0.2",
		ServiceTaggedAddresses: map[string]ServiceAddress{
			"lan": {
				Address: "127.0.0.2",
				Port:    8080,
			},
			"wan": {
				Address: "198.18.0.1",
				Port:    80,
			},
		},
		ServicePort: 8080,
		ServicePorts: ServicePorts{
			{
				Name:    "http",
				Port:    8080,
				Default: true,
			},
			{
				Name:    "https",
				Port:    8443,
				Default: false,
			},
		},
		ServiceMeta: map[string]string{
			"service": "metadata",
		},
		ServiceEnableTagOverride: true,
		RaftIndex: RaftIndex{
			CreateIndex: 1,
			ModifyIndex: 2,
		},
		ServiceProxy: TestConnectProxyConfig(t),
		ServiceConnect: ServiceConnect{
			Native: true,
		},
	}
}

func TestRegisterRequest_UnmarshalJSON_WithConnectNilDoesNotPanic(t *testing.T) {
	in := `
{
    "ID": "",
    "Node": "k8s-sync",
    "Address": "127.0.0.1",
    "TaggedAddresses": null,
    "NodeMeta": {
        "external-source": "kubernetes"
    },
    "Datacenter": "",
    "Service": {
        "Kind": "",
        "ID": "test-service-f8fd5f0f4e6c",
        "Service": "test-service",
        "Tags": [
            "k8s"
        ],
        "Meta": {
            "external-k8s-ns": "",
            "external-source": "kubernetes",
            "port-stats": "18080"
        },
        "Port": 8080,
        "Address": "192.0.2.10",
        "EnableTagOverride": false,
        "CreateIndex": 0,
        "ModifyIndex": 0,
        "Connect": null
    },
    "Check": null,
    "SkipNodeUpdate": true
}
`

	var req RegisterRequest
	err := lib.DecodeJSON(strings.NewReader(in), &req)
	require.NoError(t, err)
}

func TestNode_IsSame(t *testing.T) {
	id := types.NodeID("e62f3b31-9284-4e26-ab14-2a59dea85b55")
	node := "mynode1"
	address := ""
	datacenter := "dc1"
	n := &Node{
		ID:              id,
		Node:            node,
		Datacenter:      datacenter,
		Address:         address,
		TaggedAddresses: make(map[string]string),
		Meta:            make(map[string]string),
		RaftIndex: RaftIndex{
			CreateIndex: 1,
			ModifyIndex: 2,
		},
	}

	type testcase struct {
		name   string
		setup  func(*Node)
		expect bool
	}
	cases := []testcase{
		{
			name: "id",
			setup: func(n *Node) {
				n.ID = types.NodeID("")
			},
			expect: false,
		},
		{
			name: "node",
			setup: func(n *Node) {
				n.Node = "other"
			},
			expect: false,
		},
		{
			name: "node casing",
			setup: func(n *Node) {
				n.Node = "MyNoDe1"
			},
			expect: true,
		},
		{
			name: "dc",
			setup: func(n *Node) {
				n.Datacenter = "dcX"
			},
			expect: false,
		},
		{
			name: "address",
			setup: func(n *Node) {
				n.Address = "127.0.0.1"
			},
			expect: false,
		},
		{
			name: "tagged addresses",
			setup: func(n *Node) {
				n.TaggedAddresses = map[string]string{"my": "address"}
			},
			expect: false,
		},
		{
			name: "meta",
			setup: func(n *Node) {
				n.Meta = map[string]string{"my": "meta"}
			},
			expect: false,
		},
	}

	run := func(t *testing.T, tc testcase) {
		other := &Node{
			ID:              id,
			Node:            node,
			Datacenter:      datacenter,
			Address:         address,
			TaggedAddresses: make(map[string]string),
			Meta:            make(map[string]string),
			RaftIndex: RaftIndex{
				CreateIndex: 1,
				ModifyIndex: 3,
			},
		}

		if !n.IsSame(other) || !other.IsSame(n) {
			t.Fatalf("should be the same")
		}

		tc.setup(other)

		if tc.expect {
			if !n.IsSame(other) || !other.IsSame(n) {
				t.Fatalf("should be the same")
			}
		} else {
			if n.IsSame(other) || other.IsSame(n) {
				t.Fatalf("should be different, was %#v VS %#v", n, other)
			}
		}
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

func TestStructs_ServiceNode_IsSameService(t *testing.T) {
	const (
		nodeName = "node1"
	)

	type testcase struct {
		name   string
		setup  func(*ServiceNode)
		expect bool
	}
	cases := []testcase{
		{
			name: "ServiceID",
			setup: func(sn *ServiceNode) {
				sn.ServiceID = "66fb695a-c782-472f-8d36-4f3edd754b37"
			},
		},
		{
			name: "Node",
			setup: func(sn *ServiceNode) {
				sn.Node = "other"
			},
		},
		{
			name: "Node casing",
			setup: func(sn *ServiceNode) {
				sn.Node = "NoDe1"
			},
			expect: true,
		},
		{
			name: "ServiceAddress",
			setup: func(sn *ServiceNode) {
				sn.ServiceAddress = "1.2.3.4"
			},
		},
		{
			name: "ServiceEnableTagOverride",
			setup: func(sn *ServiceNode) {
				sn.ServiceEnableTagOverride = !sn.ServiceEnableTagOverride
			},
		},
		{
			name: "ServiceKind",
			setup: func(sn *ServiceNode) {
				sn.ServiceKind = "newKind"
			},
		},
		{
			name: "ServiceMeta",
			setup: func(sn *ServiceNode) {
				sn.ServiceMeta = map[string]string{"my": "meta"}
			},
		},
		{
			name: "ServiceName",
			setup: func(sn *ServiceNode) {
				sn.ServiceName = "duck"
			},
		},
		{
			name: "ServicePort",
			setup: func(sn *ServiceNode) {
				sn.ServicePort = 65534
			},
		},
		{
			name: "ServiceTags",
			setup: func(sn *ServiceNode) {
				sn.ServiceTags = []string{"new", "tags"}
			},
		},
		{
			name: "ServiceWeights",
			setup: func(sn *ServiceNode) {
				sn.ServiceWeights = Weights{Passing: 42, Warning: 41}
			},
		},
		{
			name: "ServiceProxy",
			setup: func(sn *ServiceNode) {
				sn.ServiceProxy = ConnectProxyConfig{}
			},
		},
		{
			name: "ServiceConnect",
			setup: func(sn *ServiceNode) {
				sn.ServiceConnect = ServiceConnect{}
			},
		},
		{
			name: "ServiceTaggedAddresses",
			setup: func(sn *ServiceNode) {
				sn.ServiceTaggedAddresses = nil
			},
		},
		{
			name: "ServiceMultiPort_Same",
			setup: func(sn *ServiceNode) {
				sn.ServicePorts = ServicePorts{
					{
						Name:    "http",
						Port:    8080,
						Default: true,
					},
					{
						Name:    "https",
						Port:    8443,
						Default: false,
					},
				}
			},
			expect: true,
		},
		{
			name: "ServiceMultiPort_Different",
			setup: func(sn *ServiceNode) {
				sn.ServicePorts = ServicePorts{
					{
						Name: "http",
						Port: 1393,
					},
				}
			},
		},
	}

	run := func(t *testing.T, tc testcase) {
		sn := testServiceNode(t)
		sn.ServiceWeights = Weights{Passing: 2, Warning: 1}
		n := sn.ToNodeService().ToServiceNode(nodeName)
		other := sn.ToNodeService().ToServiceNode(nodeName)

		if !n.IsSameService(other) || !other.IsSameService(n) {
			t.Fatalf("should be the same")
		}

		tc.setup(other)

		if tc.expect {
			if !n.IsSameService(other) || !other.IsSameService(n) {
				t.Fatalf("should be the same")
			}
		} else {
			if n.IsSameService(other) || other.IsSameService(n) {
				t.Fatalf("should be different, was %#v VS %#v", n, other)
			}
		}
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

func TestStructs_ServiceNode_PartialClone(t *testing.T) {
	sn := testServiceNode(t)

	clone := sn.PartialClone()

	// Make sure the parts that weren't supposed to be cloned didn't get
	// copied over, then zero-value them out so we can do a DeepEqual() on
	// the rest of the contents.
	if clone.ID != "" ||
		clone.Address != "" ||
		clone.Datacenter != "" ||
		len(clone.TaggedAddresses) != 0 ||
		len(clone.NodeMeta) != 0 {
		t.Fatalf("bad: %v", clone)
	}

	sn.ID = ""
	sn.Address = ""
	sn.Datacenter = ""
	sn.TaggedAddresses = nil
	sn.NodeMeta = nil
	require.Equal(t, sn, clone)

	sn.ServiceTags = append(sn.ServiceTags, "hello")
	if reflect.DeepEqual(sn, clone) {
		t.Fatalf("clone wasn't independent of the original")
	}

	revert := make([]string, len(sn.ServiceTags)-1)
	copy(revert, sn.ServiceTags[0:len(sn.ServiceTags)-1])
	sn.ServiceTags = revert
	if !reflect.DeepEqual(sn, clone) {
		t.Fatalf("bad: %v VS %v", clone, sn)
	}
	oldPassingWeight := clone.ServiceWeights.Passing
	sn.ServiceWeights.Passing = 1000
	if reflect.DeepEqual(sn, clone) {
		t.Fatalf("clone wasn't independent of the original for Meta")
	}
	sn.ServiceWeights.Passing = oldPassingWeight
	sn.ServiceMeta["new_meta"] = "new_value"
	if reflect.DeepEqual(sn, clone) {
		t.Fatalf("clone wasn't independent of the original for Meta")
	}

	// ensure that the tagged addresses were copied and not just a pointer to the map
	sn.ServiceTaggedAddresses["foo"] = ServiceAddress{Address: "consul.is.awesome", Port: 443}
	require.NotEqual(t, sn, clone)
}

func TestStructs_ServiceNode_Conversions(t *testing.T) {
	sn := testServiceNode(t)

	sn2 := sn.ToNodeService().ToServiceNode("node1")

	// These two fields get lost in the conversion, so we have to zero-value
	// them out before we do the compare.
	sn.ID = ""
	sn.Address = ""
	sn.Datacenter = ""
	sn.TaggedAddresses = nil
	sn.NodeMeta = nil
	sn.ServiceWeights = Weights{Passing: 1, Warning: 1}
	require.Equal(t, sn, sn2)
	if !sn.IsSameService(sn2) || !sn2.IsSameService(sn) {
		t.Fatalf("bad: %#v, should be the same %#v", sn2, sn)
	}
	// Those fields are lost in conversion, so IsSameService() should not take them into account
	sn.Address = "y"
	sn.Datacenter = "z"
	sn.TaggedAddresses = map[string]string{"one": "1", "two": "2"}
	sn.NodeMeta = map[string]string{"meta": "data"}
	if !sn.IsSameService(sn2) || !sn2.IsSameService(sn) {
		t.Fatalf("bad: %#v, should be the same %#v", sn2, sn)
	}
}

func TestStructs_Locality_Validate(t *testing.T) {
	type testCase struct {
		locality *Locality
		err      string
	}
	cases := map[string]testCase{
		"nil": {
			nil,
			"",
		},
		"region only": {
			&Locality{Region: "us-west-1"},
			"",
		},
		"region and zone": {
			&Locality{Region: "us-west-1", Zone: "us-west-1a"},
			"",
		},
		"zone only": {
			&Locality{Zone: "us-west-1a"},
			"zone cannot be set without region",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := tc.locality.Validate()
			if tc.err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.err)
			}
		})
	}
}

func TestStructs_NodeService_ValidateMultiPort(t *testing.T) {
	type testCase struct {
		Ns  NodeService
		Err string
	}

	cases := map[string]testCase{
		"valid": {
			Ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "http",
						Port:    80,
						Default: true,
					},
					{
						Name:    "https",
						Port:    443,
						Default: false,
					},
				},
			},
			Err: "",
		},
		"no-default": {
			Ns: NodeService{
				Ports: ServicePorts{
					{
						Name: "http",
						Port: 80,
					},
					{
						Name: "https",
						Port: 443,
					},
				},
			},
			Err: "One of the Ports must be marked as Default",
		},
		"multiple-default": {
			Ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "http",
						Port:    80,
						Default: true,
					},
					{
						Name:    "https",
						Port:    443,
						Default: true,
					},
				},
			},
			Err: "Only one port can be marked as default",
		},
		"duplicate-names": {
			Ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "http",
						Port:    80,
						Default: true,
					},
					{
						Name:    "http",
						Port:    8080,
						Default: false,
					},
				},
			},
			Err: "Ports.Name \"http\" has to be unique",
		},
		"empty-name": {
			Ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "",
						Port:    80,
						Default: true,
					},
					{
						Name:    "https",
						Port:    80,
						Default: false,
					},
				},
			},
			Err: "Ports.Name cannot be empty",
		},
		"zero-port": {
			Ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "http",
						Port:    0,
						Default: true,
					},
					{
						Name:    "https",
						Port:    80,
						Default: false,
					},
				},
			},
			Err: "ports.port must be greater than zero",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := tc.Ns.Validate()

			if tc.Err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
			}
		})
	}
}

func TestStructs_NodeService_ValidateMeshGateway(t *testing.T) {
	type testCase struct {
		Modify func(*NodeService)
		Err    string
	}
	cases := map[string]testCase{
		"valid": {
			func(x *NodeService) {},
			"",
		},
		"zero-port": {
			func(x *NodeService) { x.Port = 0 },
			"Port must be non-zero",
		},
		"sidecar-service": {
			func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} },
			"cannot have a sidecar service",
		},
		"proxy-destination-name": {
			func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" },
			"Proxy.DestinationServiceName configuration is invalid",
		},
		"proxy-destination-id": {
			func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" },
			"Proxy.DestinationServiceID configuration is invalid",
		},
		"proxy-local-address": {
			func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" },
			"Proxy.LocalServiceAddress configuration is invalid",
		},
		"proxy-local-port": {
			func(x *NodeService) { x.Proxy.LocalServicePort = 36 },
			"Proxy.LocalServicePort configuration is invalid",
		},
		"proxy-upstreams": {
			func(x *NodeService) { x.Proxy.Upstreams = []Upstream{{}} },
			"Proxy.Upstreams configuration is invalid",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			ns := TestNodeServiceMeshGateway(t)
			tc.Modify(ns)

			err := ns.Validate()
			if tc.Err == "" {
				require.NoError(t, err)
			} else {
				require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
			}
		})
	}
}

func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) {
	type testCase struct {
		Modify func(*NodeService)
		Err    string
	}

	cases := map[string]testCase{
		"valid": {
			func(x *NodeService) {},
			"",
		},
		"sidecar-service": {
			func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} },
			"cannot have a sidecar service",
		},
		"proxy-destination-name": {
			func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" },
			"Proxy.DestinationServiceName configuration is invalid",
		},
		"proxy-destination-id": {
			func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" },
			"Proxy.DestinationServiceID configuration is invalid",
		},
		"proxy-local-address": {
			func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" },
			"Proxy.LocalServiceAddress configuration is invalid",
		},
		"proxy-local-port": {
			func(x *NodeService) { x.Proxy.LocalServicePort = 36 },
			"Proxy.LocalServicePort configuration is invalid",
		},
		"proxy-upstreams": {
			func(x *NodeService) { x.Proxy.Upstreams = []Upstream{{}} },
			"Proxy.Upstreams configuration is invalid",
		},
		"port": {
			func(x *NodeService) { x.Port = 0 },
			"Port must be non-zero",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			ns := TestNodeServiceTerminatingGateway(t, "10.0.0.5")
			tc.Modify(ns)

			err := ns.Validate()
			if tc.Err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
			}
		})
	}
}

func TestStructs_NodeService_ValidateIngressGateway(t *testing.T) {
	type testCase struct {
		Modify func(*NodeService)
		Err    string
	}

	cases := map[string]testCase{
		"valid": {
			func(x *NodeService) {},
			"",
		},
		"sidecar-service": {
			func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} },
			"cannot have a sidecar service",
		},
		"proxy-destination-name": {
			func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" },
			"Proxy.DestinationServiceName configuration is invalid",
		},
		"proxy-destination-id": {
			func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" },
			"Proxy.DestinationServiceID configuration is invalid",
		},
		"proxy-local-address": {
			func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" },
			"Proxy.LocalServiceAddress configuration is invalid",
		},
		"proxy-local-port": {
			func(x *NodeService) { x.Proxy.LocalServicePort = 36 },
			"Proxy.LocalServicePort configuration is invalid",
		},
		"proxy-upstreams": {
			func(x *NodeService) { x.Proxy.Upstreams = []Upstream{{}} },
			"Proxy.Upstreams configuration is invalid",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			ns := TestNodeServiceIngressGateway(t, "10.0.0.5")
			tc.Modify(ns)

			err := ns.Validate()
			if tc.Err == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
			}
		})
	}
}

func TestStructs_NodeService_ValidateExposeConfig(t *testing.T) {
	type testCase struct {
		Modify func(*NodeService)
		Err    string
	}
	cases := map[string]testCase{
		"valid": {
			Modify: func(x *NodeService) {},
			Err:    "",
		},
		"empty path": {
			Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].Path = "" },
			Err:    "empty path exposed",
		},
		"invalid port negative": {
			Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].ListenerPort = -1 },
			Err:    "invalid listener port",
		},
		"invalid port too large": {
			Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].ListenerPort = 65536 },
			Err:    "invalid listener port",
		},
		"duplicate paths are allowed": {
			Modify: func(x *NodeService) {
				x.Proxy.Expose.Paths[0].Path = "/healthz"
				x.Proxy.Expose.Paths[1].Path = "/healthz"
			},
			Err: "",
		},
		"duplicate listener ports are not allowed": {
			Modify: func(x *NodeService) {
				x.Proxy.Expose.Paths[0].ListenerPort = 21600
				x.Proxy.Expose.Paths[1].ListenerPort = 21600
			},
			Err: "duplicate listener ports exposed",
		},
		"protocol not supported": {
			Modify: func(x *NodeService) { x.Proxy.Expose.Paths[0].Protocol = "foo" },
			Err:    "protocol 'foo' not supported for path",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			ns := TestNodeServiceExpose(t)
			tc.Modify(ns)

			err := ns.Validate()
			if tc.Err == "" {
				require.NoError(t, err)
			} else {
				require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
			}
		})
	}
}

func TestStructs_NodeService_ValidateConnectProxy(t *testing.T) {
	cases := []struct {
		Name   string
		Modify func(*NodeService)
		Err    string
	}{
		{
			"valid",
			func(x *NodeService) {},
			"",
		},

		{
			"connect-proxy: invalid opaque config",
			func(x *NodeService) {
				x.Proxy.Config = map[string]interface{}{
					"envoy_hcp_metrics_bind_socket_dir": "/Consul/is/a/networking/platform/that/enables/securing/your/networking/",
				}
			},
			"Proxy.Config: envoy_hcp_metrics_bind_socket_dir length 71 exceeds max",
		},

		{
			"connect-proxy: no Proxy.DestinationServiceName",
			func(x *NodeService) { x.Proxy.DestinationServiceName = "" },
			"Proxy.DestinationServiceName must be",
		},

		{
			"connect-proxy: whitespace Proxy.DestinationServiceName",
			func(x *NodeService) { x.Proxy.DestinationServiceName = "  " },
			"Proxy.DestinationServiceName must be",
		},

		{
			"connect-proxy: wildcard Proxy.DestinationServiceName",
			func(x *NodeService) { x.Proxy.DestinationServiceName = "*" },
			"Proxy.DestinationServiceName must not be",
		},

		{
			"connect-proxy: valid Proxy.DestinationServiceName",
			func(x *NodeService) { x.Proxy.DestinationServiceName = "hello" },
			"",
		},

		{
			"connect-proxy: no port set",
			func(x *NodeService) { x.Port = 0 },
			fmt.Sprintf("Port or SocketPath must be set for a %s", ServiceKindConnectProxy),
		},

		{
			"connect-proxy: ConnectNative set",
			func(x *NodeService) { x.Connect.Native = true },
			"cannot also be",
		},

		{
			"connect-proxy: upstream missing type (defaulted)",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationName: "foo",
					LocalBindPort:   5000,
				}}
			},
			"",
		},
		{
			"connect-proxy: upstream invalid type",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationType: "garbage",
					DestinationName: "foo",
					LocalBindPort:   5000,
				}}
			},
			"unknown upstream destination type",
		},
		{
			"connect-proxy: upstream empty name",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationType: UpstreamDestTypeService,
					LocalBindPort:   5000,
				}}
			},
			"upstream destination name cannot be empty",
		},
		{
			"connect-proxy: upstream wildcard name",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationType: UpstreamDestTypeService,
					DestinationName: WildcardSpecifier,
					LocalBindPort:   5000,
				}}
			},
			"upstream destination name cannot be a wildcard",
		},
		{
			"connect-proxy: upstream can have wildcard name when centrally configured",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationType:     UpstreamDestTypeService,
					DestinationName:     WildcardSpecifier,
					CentrallyConfigured: true,
				}}
			},
			"",
		},
		{
			"connect-proxy: upstream empty bind port",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationType: UpstreamDestTypeService,
					DestinationName: "foo",
					LocalBindPort:   0,
				}}
			},
			"upstream local bind port or local socket path must be defined and nonzero",
		},
		{
			"connect-proxy: upstream bind port and path defined",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{{
					DestinationType:     UpstreamDestTypeService,
					DestinationName:     "foo",
					LocalBindPort:       1,
					LocalBindSocketPath: "/tmp/socket",
				}}
			},
			"only one of upstream local bind port or local socket path can be defined and nonzero",
		},
		{
			"connect-proxy: Upstreams almost-but-not-quite-duplicated in various ways",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{ // baseline
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						LocalBindPort:   5000,
					},
					{ // different bind address
						DestinationType:  UpstreamDestTypeService,
						DestinationName:  "bar",
						LocalBindAddress: "127.0.0.2",
						LocalBindPort:    5000,
					},
					{ // different datacenter
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						Datacenter:      "dc2",
						LocalBindPort:   5001,
					},
					{ // explicit default namespace
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationNamespace: "default",
						LocalBindPort:        5003,
					},
					{ // different namespace
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationNamespace: "alternate",
						LocalBindPort:        5002,
					},
					{ // different type
						DestinationType: UpstreamDestTypePreparedQuery,
						DestinationName: "foo",
						LocalBindPort:   5004,
					},
				}
			},
			"",
		},
		{
			"connect-proxy: Upstreams non default partition another dc",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{ // baseline
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationPartition: "foo",
						Datacenter:           "dc1",
						LocalBindPort:        5000,
					},
				}
			},
			"upstreams cannot target another datacenter in non default partition",
		},
		{
			"connect-proxy: Upstreams duplicated by port",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						LocalBindPort:   5000,
					},
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						LocalBindPort:   5000,
					},
				}
			},
			"upstreams cannot contain duplicates",
		},
		{
			"connect-proxy: Centrally configured upstreams can have duplicate ip/port",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType:     UpstreamDestTypeService,
						DestinationName:     "foo",
						CentrallyConfigured: true,
					},
					{
						DestinationType:     UpstreamDestTypeService,
						DestinationName:     "bar",
						CentrallyConfigured: true,
					},
				}
			},
			"",
		},
		{
			"connect-proxy: Upstreams duplicated by ip and port",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType:  UpstreamDestTypeService,
						DestinationName:  "foo",
						LocalBindAddress: "127.0.0.2",
						LocalBindPort:    5000,
					},
					{
						DestinationType:  UpstreamDestTypeService,
						DestinationName:  "bar",
						LocalBindAddress: "127.0.0.2",
						LocalBindPort:    5000,
					},
				}
			},
			"upstreams cannot contain duplicates",
		},
		{
			"connect-proxy: Upstreams duplicated by ip and port with ip defaulted in one",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						LocalBindPort:   5000,
					},
					{
						DestinationType:  UpstreamDestTypeService,
						DestinationName:  "foo",
						LocalBindAddress: "127.0.0.1",
						LocalBindPort:    5000,
					},
				}
			},
			"upstreams cannot contain duplicates",
		},
		{
			"connect-proxy: Upstreams duplicated by name",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						LocalBindPort:   5000,
					},
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						LocalBindPort:   5001,
					},
				}
			},
			"upstreams cannot contain duplicates",
		},
		{
			"connect-proxy: Upstreams duplicated by name and datacenter",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						Datacenter:      "dc2",
						LocalBindPort:   5000,
					},
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						Datacenter:      "dc2",
						LocalBindPort:   5001,
					},
				}
			},
			"upstreams cannot contain duplicates",
		},
		{
			"connect-proxy: Upstreams duplicated by name and namespace",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationNamespace: "alternate",
						LocalBindPort:        5000,
					},
					{
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationNamespace: "alternate",
						LocalBindPort:        5001,
					},
				}
			},
			"upstreams cannot contain duplicates",
		},
		{
			"connect-proxy: valid Upstream.PeerDestination",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						DestinationPeer: "peer1",
						LocalBindPort:   5000,
					},
				}
			},
			"",
		},
		{
			"connect-proxy: invalid locality",
			func(x *NodeService) {
				x.Locality = &Locality{Zone: "bad"}
			},
			"zone cannot be set without region",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ns := TestNodeServiceProxy(t)
			tc.Modify(ns)

			err := ns.Validate()
			assert.Equal(t, err != nil, tc.Err != "", err)
			if err == nil {
				return
			}

			assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
		})
	}
}

func TestStructs_NodeService_ValidateConnectProxyWithAgentAutoAssign(t *testing.T) {
	t.Run("connect-proxy: no port set", func(t *testing.T) {
		ns := TestNodeServiceProxy(t)
		ns.Port = 0

		err := ns.ValidateForAgent()
		assert.NoError(t, err)
	})
}

func TestStructs_NodeService_ValidateConnectProxy_In_Partition(t *testing.T) {
	cases := []struct {
		Name   string
		Modify func(*NodeService)
		Err    string
	}{
		{
			"valid",
			func(x *NodeService) {},
			"",
		},
		{
			"connect-proxy: Upstreams non default partition another dc",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{ // baseline
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationPartition: "foo",
						Datacenter:           "dc1",
						LocalBindPort:        5000,
					},
				}
			},
			"upstreams cannot target another datacenter in non default partition",
		},
		{
			"connect-proxy: Upstreams non default partition same dc",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{ // baseline
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationPartition: "foo",
						LocalBindPort:        5000,
					},
				}
			},
			"",
		},
		{
			"connect-proxy: Upstream with peer targets partition different from NodeService",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType:      UpstreamDestTypeService,
						DestinationName:      "foo",
						DestinationPartition: "part1",
						DestinationPeer:      "peer1",
						LocalBindPort:        5000,
					},
				}
			},
			"upstreams must target peers in the same partition as the service",
		},
		{
			"connect-proxy: Upstream with peer defaults to NodeService's peer",
			func(x *NodeService) {
				x.Proxy.Upstreams = Upstreams{
					{
						DestinationType: UpstreamDestTypeService,
						DestinationName: "foo",
						// No DestinationPartition here but we assert that it defaults to "bar" and not "default"
						DestinationPeer: "peer1",
						LocalBindPort:   5000,
					},
				}
			},
			"",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ns := TestNodeServiceProxyInPartition(t, "bar")
			tc.Modify(ns)

			err := ns.Validate()
			assert.Equal(t, err != nil, tc.Err != "", err)
			if err == nil {
				return
			}

			assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
		})
	}
}

func TestStructs_NodeService_ValidateSidecarService(t *testing.T) {
	cases := []struct {
		Name   string
		Modify func(*NodeService)
		Err    string
	}{
		{
			"valid",
			func(x *NodeService) {},
			"",
		},

		{
			"ID can't be set",
			func(x *NodeService) { x.Connect.SidecarService.ID = "foo" },
			"SidecarService cannot specify an ID",
		},

		{
			"Nested sidecar can't be set",
			func(x *NodeService) {
				x.Connect.SidecarService.Connect = &ServiceConnect{
					SidecarService: &ServiceDefinition{},
				}
			},
			"SidecarService cannot have a nested SidecarService",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ns := TestNodeServiceSidecar(t)
			tc.Modify(ns)

			err := ns.Validate()
			assert.Equal(t, err != nil, tc.Err != "", err)
			if err == nil {
				return
			}

			assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
		})
	}
}

func TestStructs_NodeService_ConnectNativeEmptyPortError(t *testing.T) {
	ns := TestNodeService()
	ns.Connect.Native = true
	ns.Port = 0
	err := ns.Validate()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "Port or SocketPath must be set for a Connect native service.")
}

func TestStructs_NodeService_IsSame(t *testing.T) {
	ns := &NodeService{
		ID:      "node1",
		Service: "theservice",
		Tags:    []string{"foo", "bar"},
		Address: "127.0.0.1",
		TaggedAddresses: map[string]ServiceAddress{
			"lan": {
				Address: "127.0.0.1",
				Port:    3456,
			},
			"wan": {
				Address: "198.18.0.1",
				Port:    1234,
			},
		},
		Meta: map[string]string{
			"meta1": "value1",
			"meta2": "value2",
		},
		Port: 1234,
		Ports: ServicePorts{
			{
				Name: "http",
				Port: 8080,
			},
			{
				Name: "metrics",
				Port: 9090,
			},
		},
		EnableTagOverride: true,
		Proxy: ConnectProxyConfig{
			DestinationServiceName: "db",
			Config: map[string]interface{}{
				"foo": "bar",
			},
		},
		Weights: &Weights{Passing: 1, Warning: 1},
	}
	if !ns.IsSame(ns) {
		t.Fatalf("should be equal to itself")
	}

	other := &NodeService{
		ID:      "node1",
		Service: "theservice",
		Tags:    []string{"foo", "bar"},
		Address: "127.0.0.1",
		Port:    1234,
		Ports: ServicePorts{
			{
				Name: "http",
				Port: 8080,
			},
			{
				Name: "metrics",
				Port: 9090,
			},
		},
		EnableTagOverride: true,
		TaggedAddresses: map[string]ServiceAddress{
			"wan": {
				Address: "198.18.0.1",
				Port:    1234,
			},
			"lan": {
				Address: "127.0.0.1",
				Port:    3456,
			},
		},
		Meta: map[string]string{
			// We don't care about order
			"meta2": "value2",
			"meta1": "value1",
		},
		Proxy: ConnectProxyConfig{
			DestinationServiceName: "db",
			Config: map[string]interface{}{
				"foo": "bar",
			},
		},
		Weights: &Weights{Passing: 1, Warning: 1},
		RaftIndex: RaftIndex{
			CreateIndex: 1,
			ModifyIndex: 2,
		},
	}
	if !ns.IsSame(other) || !other.IsSame(ns) {
		t.Fatalf("should not care about Raft fields")
	}

	check := func(twiddle, restore func()) {
		t.Helper()
		if !ns.IsSame(other) || !other.IsSame(ns) {
			t.Fatalf("should be the same")
		}

		twiddle()
		if ns.IsSame(other) || other.IsSame(ns) {
			t.Fatalf("should not be the same")
		}

		restore()
		if !ns.IsSame(other) || !other.IsSame(ns) {
			t.Fatalf("should be the same again")
		}
	}

	check(func() { other.ID = "XXX" }, func() { other.ID = "node1" })
	check(func() { other.Service = "XXX" }, func() { other.Service = "theservice" })
	check(func() { other.Tags = nil }, func() { other.Tags = []string{"foo", "bar"} })
	check(func() { other.Tags = []string{"foo"} }, func() { other.Tags = []string{"foo", "bar"} })
	check(func() { other.Address = "XXX" }, func() { other.Address = "127.0.0.1" })
	check(func() { other.Port = 9999 }, func() { other.Port = 1234 })
	check(func() { other.Meta["meta2"] = "wrongValue" }, func() { other.Meta["meta2"] = "value2" })
	check(func() { other.EnableTagOverride = false }, func() { other.EnableTagOverride = true })
	check(func() { other.Kind = ServiceKindConnectProxy }, func() { other.Kind = "" })
	check(func() { other.Proxy.DestinationServiceName = "" }, func() { other.Proxy.DestinationServiceName = "db" })
	check(func() { other.Proxy.DestinationServiceID = "XXX" }, func() { other.Proxy.DestinationServiceID = "" })
	check(func() { other.Proxy.LocalServiceAddress = "XXX" }, func() { other.Proxy.LocalServiceAddress = "" })
	check(func() { other.Proxy.LocalServicePort = 9999 }, func() { other.Proxy.LocalServicePort = 0 })
	check(func() { other.Proxy.Config["baz"] = "XXX" }, func() { delete(other.Proxy.Config, "baz") })
	check(func() { other.Connect.Native = true }, func() { other.Connect.Native = false })
	otherServiceNode := other.ToServiceNode("node1")
	copyNodeService := otherServiceNode.ToNodeService()
	if !copyNodeService.IsSame(other) {
		t.Fatalf("copy should be the same, but was\n %#v\nVS\n %#v", copyNodeService, other)
	}
	otherServiceNodeCopy2 := copyNodeService.ToServiceNode("node1")
	if !otherServiceNode.IsSameService(otherServiceNodeCopy2) {
		t.Fatalf("copy should be the same, but was\n %#v\nVS\n %#v", otherServiceNode, otherServiceNodeCopy2)
	}
	check(func() { other.TaggedAddresses["lan"] = ServiceAddress{Address: "127.0.0.1", Port: 9999} }, func() { other.TaggedAddresses["lan"] = ServiceAddress{Address: "127.0.0.1", Port: 3456} })
	check(func() { other.Ports[0].Name = "port1" }, func() { other.Ports[0].Name = "http" })
}

func TestStructs_NodeService_DefaultPort(t *testing.T) {
	type testCase struct {
		ns       NodeService
		expected int
	}

	cases := map[string]testCase{
		"no_ports_with_port": {
			ns: NodeService{
				Port: 8080,
			},
			expected: 8080,
		},
		"no_ports_no_port": {
			ns:       NodeService{},
			expected: 0,
		},
		"with_ports_no_port": {
			ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "http",
						Port:    8080,
						Default: true,
					},
				},
			},
			expected: 8080,
		},
		"with_ports_with_port": {
			ns: NodeService{
				Ports: ServicePorts{
					{
						Name:    "http",
						Port:    8080,
						Default: true,
					},
				},
				Port: 8081,
			},
			expected: 8080,
		},
		// Although this scenario isn't possible, helps improve coverage :D
		"only_ports_no_default": {
			ns: NodeService{
				Ports: ServicePorts{
					{
						Name: "http",
						Port: 8080,
					},
				},
			},
			expected: 0,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			actual := tc.ns.DefaultPort()
			require.Equal(t, tc.expected, actual)
		})
	}
}

func TestStructs_HealthCheck_IsSame(t *testing.T) {
	type testcase struct {
		name   string
		setup  func(*HealthCheck)
		expect bool
	}

	cases := []testcase{
		{
			name: "Node",
			setup: func(hc *HealthCheck) {
				hc.Node = "XXX"
			},
		},
		{
			name: "Node casing",
			setup: func(hc *HealthCheck) {
				hc.Node = "NoDe1"
			},
			expect: true,
		},
		{
			name: "CheckID",
			setup: func(hc *HealthCheck) {
				hc.CheckID = "XXX"
			},
		},
		{
			name: "Name",
			setup: func(hc *HealthCheck) {
				hc.Name = "XXX"
			},
		},
		{
			name: "Status",
			setup: func(hc *HealthCheck) {
				hc.Status = "XXX"
			},
		},
		{
			name: "Notes",
			setup: func(hc *HealthCheck) {
				hc.Notes = "XXX"
			},
		},
		{
			name: "Output",
			setup: func(hc *HealthCheck) {
				hc.Output = "XXX"
			},
		},
		{
			name: "ServiceID",
			setup: func(hc *HealthCheck) {
				hc.ServiceID = "XXX"
			},
		},
		{
			name: "ServiceName",
			setup: func(hc *HealthCheck) {
				hc.ServiceName = "XXX"
			},
		},
	}

	run := func(t *testing.T, tc testcase) {
		hc := &HealthCheck{
			Node:        "node1",
			CheckID:     "check1",
			Name:        "thecheck",
			Status:      api.HealthPassing,
			Notes:       "it's all good",
			Output:      "lgtm",
			ServiceID:   "service1",
			ServiceName: "theservice",
			ServiceTags: []string{"foo"},
		}

		if !hc.IsSame(hc) {
			t.Fatalf("should be equal to itself")
		}

		other := &HealthCheck{
			Node:        "node1",
			CheckID:     "check1",
			Name:        "thecheck",
			Status:      api.HealthPassing,
			Notes:       "it's all good",
			Output:      "lgtm",
			ServiceID:   "service1",
			ServiceName: "theservice",
			ServiceTags: []string{"foo"},
			RaftIndex: RaftIndex{
				CreateIndex: 1,
				ModifyIndex: 2,
			},
		}

		if !hc.IsSame(other) || !other.IsSame(hc) {
			t.Fatalf("should not care about Raft fields")
		}

		tc.setup(hc)

		if tc.expect {
			if !hc.IsSame(other) || !other.IsSame(hc) {
				t.Fatalf("should be the same")
			}
		} else {
			if hc.IsSame(other) || other.IsSame(hc) {
				t.Fatalf("should not be the same")
			}
		}
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

func TestStructs_HealthCheck_Marshalling(t *testing.T) {
	d := &HealthCheckDefinition{}
	buf, err := d.MarshalJSON()
	require.NoError(t, err)
	require.NotContains(t, string(buf), `"Interval":""`)
	require.NotContains(t, string(buf), `"Timeout":""`)
	require.NotContains(t, string(buf), `"DeregisterCriticalServiceAfter":""`)
}

func TestStructs_HealthCheck_Clone(t *testing.T) {
	hc := &HealthCheck{
		Node:        "node1",
		CheckID:     "check1",
		Name:        "thecheck",
		Status:      api.HealthPassing,
		Notes:       "it's all good",
		Output:      "lgtm",
		ServiceID:   "service1",
		ServiceName: "theservice",
	}
	clone := hc.Clone()
	if !hc.IsSame(clone) {
		t.Fatalf("should be equal to its clone")
	}

	clone.Output = "different"
	if hc.IsSame(clone) {
		t.Fatalf("should not longer be equal to its clone")
	}
}

func TestCheckServiceNodes_Shuffle(t *testing.T) {
	// Make a huge list of nodes.
	var nodes CheckServiceNodes
	for i := 0; i < 100; i++ {
		nodes = append(nodes, CheckServiceNode{
			Node: &Node{
				Node:    fmt.Sprintf("node%d", i),
				Address: fmt.Sprintf("127.0.0.%d", i+1),
			},
		})
	}

	// Keep track of how many unique shuffles we get.
	uniques := make(map[string]struct{})
	for i := 0; i < 100; i++ {
		nodes.Shuffle()

		var names []string
		for _, node := range nodes {
			names = append(names, node.Node.Node)
		}
		key := strings.Join(names, "|")
		uniques[key] = struct{}{}
	}

	// We have to allow for the fact that there won't always be a unique
	// shuffle each pass, so we just look for smell here without the test
	// being flaky.
	if len(uniques) < 50 {
		t.Fatalf("unique shuffle ratio too low: %d/100", len(uniques))
	}
}

func TestCheckServiceNodes_Filter(t *testing.T) {
	nodes := CheckServiceNodes{
		CheckServiceNode{
			Node: &Node{
				Node:    "node1",
				Address: "127.0.0.1",
			},
			Checks: HealthChecks{
				&HealthCheck{
					Status: api.HealthWarning,
				},
			},
		},
		CheckServiceNode{
			Node: &Node{
				Node:    "node2",
				Address: "127.0.0.2",
			},
			Checks: HealthChecks{
				&HealthCheck{
					Status: api.HealthPassing,
				},
			},
		},
		CheckServiceNode{
			Node: &Node{
				Node:    "node3",
				Address: "127.0.0.3",
			},
			Checks: HealthChecks{
				&HealthCheck{
					Status: api.HealthCritical,
				},
			},
		},
		CheckServiceNode{
			Node: &Node{
				Node:    "node4",
				Address: "127.0.0.4",
			},
			Checks: HealthChecks{
				// This check has a different ID to the others to ensure it is not
				// ignored by accident
				&HealthCheck{
					CheckID: "failing2",
					Status:  api.HealthCritical,
				},
			},
		},
	}

	// Test the case where warnings are allowed.
	{
		twiddle := make(CheckServiceNodes, len(nodes))
		if n := copy(twiddle, nodes); n != len(nodes) {
			t.Fatalf("bad: %d", n)
		}
		filtered := twiddle.Filter(CheckServiceNodeFilterOptions{FilterType: HealthFilterExcludeCritical})
		expected := CheckServiceNodes{
			nodes[0],
			nodes[1],
		}
		if !reflect.DeepEqual(filtered, expected) {
			t.Fatalf("bad: %v", filtered)
		}
	}

	// Limit to only passing checks.
	{
		twiddle := make(CheckServiceNodes, len(nodes))
		if n := copy(twiddle, nodes); n != len(nodes) {
			t.Fatalf("bad: %d", n)
		}
		filtered := twiddle.Filter(CheckServiceNodeFilterOptions{FilterType: HealthFilterIncludeOnlyPassing})
		expected := CheckServiceNodes{
			nodes[1],
		}
		if !reflect.DeepEqual(filtered, expected) {
			t.Fatalf("bad: %v", filtered)
		}
	}

	// Allow failing checks to be ignored (note that the test checks have empty
	// CheckID which is valid).
	{
		twiddle := make(CheckServiceNodes, len(nodes))
		if n := copy(twiddle, nodes); n != len(nodes) {
			t.Fatalf("bad: %d", n)
		}
		filtered := twiddle.Filter(CheckServiceNodeFilterOptions{FilterType: HealthFilterIncludeOnlyPassing, IgnoreCheckIDs: []types.CheckID{""}})
		expected := CheckServiceNodes{
			nodes[0],
			nodes[1],
			nodes[2], // Node 3's critical check should be ignored.
			// Node 4 should still be failing since it's got a critical check with a
			// non-ignored ID.
		}
		if !reflect.DeepEqual(filtered, expected) {
			t.Fatalf("bad: %v", filtered)
		}
	}
}

func TestCheckServiceNode_CanRead(t *testing.T) {
	type testCase struct {
		name     string
		csn      CheckServiceNode
		authz    acl.Authorizer
		expected acl.EnforcementDecision
	}

	fn := func(t *testing.T, tc testCase) {
		actual := tc.csn.CanRead(tc.authz)
		require.Equal(t, tc.expected, actual)
	}

	var testCases = []testCase{
		{
			name:     "empty",
			expected: acl.Deny,
		},
		{
			name: "node read not authorized",
			csn: CheckServiceNode{
				Node:    &Node{Node: "name"},
				Service: &NodeService{Service: "service-name"},
			},
			authz:    aclAuthorizerCheckServiceNode{allowLocalService: true},
			expected: acl.Deny,
		},
		{
			name: "service read not authorized",
			csn: CheckServiceNode{
				Node:    &Node{Node: "name"},
				Service: &NodeService{Service: "service-name"},
			},
			authz:    aclAuthorizerCheckServiceNode{allowLocalNode: true},
			expected: acl.Deny,
		},
		{
			name: "read authorized",
			csn: CheckServiceNode{
				Node:    &Node{Node: "name"},
				Service: &NodeService{Service: "service-name"},
			},
			authz:    acl.AllowAll(),
			expected: acl.Allow,
		},
		{
			name: "can read imported csn if can read imported data",
			csn: CheckServiceNode{
				Node:    &Node{Node: "name", PeerName: "cluster-2"},
				Service: &NodeService{Service: "service-name", PeerName: "cluster-2"},
			},
			authz:    aclAuthorizerCheckServiceNode{allowImported: true},
			expected: acl.Allow,
		},
		{
			name: "can't read imported csn with authz for local services and nodes",
			csn: CheckServiceNode{
				Node:    &Node{Node: "name", PeerName: "cluster-2"},
				Service: &NodeService{Service: "service-name", PeerName: "cluster-2"},
			},
			authz:    aclAuthorizerCheckServiceNode{allowLocalService: true, allowLocalNode: true},
			expected: acl.Deny,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			fn(t, tc)
		})
	}
}

type aclAuthorizerCheckServiceNode struct {
	acl.Authorizer
	allowLocalNode    bool
	allowLocalService bool
	allowImported     bool
}

func (a aclAuthorizerCheckServiceNode) ServiceRead(_ string, ctx *acl.AuthorizerContext) acl.EnforcementDecision {
	if ctx.Peer != "" {
		if a.allowImported {
			return acl.Allow
		}
		return acl.Deny
	}

	if a.allowLocalService {
		return acl.Allow
	}
	return acl.Deny
}

func (a aclAuthorizerCheckServiceNode) NodeRead(_ string, ctx *acl.AuthorizerContext) acl.EnforcementDecision {
	if ctx.Peer != "" {
		if a.allowImported {
			return acl.Allow
		}
		return acl.Deny
	}

	if a.allowLocalNode {
		return acl.Allow
	}
	return acl.Deny
}

func TestStructs_DirEntry_Clone(t *testing.T) {
	e := &DirEntry{
		LockIndex: 5,
		Key:       "hello",
		Flags:     23,
		Value:     []byte("this is a test"),
		Session:   "session1",
		RaftIndex: RaftIndex{
			CreateIndex: 1,
			ModifyIndex: 2,
		},
	}

	clone := e.Clone()
	if !reflect.DeepEqual(e, clone) {
		t.Fatalf("bad: %v", clone)
	}

	e.Value = []byte("a new value")
	if reflect.DeepEqual(e, clone) {
		t.Fatalf("clone wasn't independent of the original")
	}
}

func TestStructs_ValidateServiceAndNodeMetadata(t *testing.T) {
	tooMuchMeta := make(map[string]string)
	for i := 0; i < metaMaxKeyPairs+1; i++ {
		tooMuchMeta[fmt.Sprint(i)] = "value"
	}
	type testcase struct {
		Meta              map[string]string
		AllowConsulPrefix bool
		NodeError         string
		ServiceError      string
		GatewayError      string
	}
	cases := map[string]testcase{
		"should succeed": {
			map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			false,
			"",
			"",
			"",
		},
		"invalid key": {
			map[string]string{
				"": "value1",
			},
			false,
			"Couldn't load metadata pair",
			"Couldn't load metadata pair",
			"Couldn't load metadata pair",
		},
		"too many keys": {
			tooMuchMeta,
			false,
			"cannot contain more than",
			"cannot contain more than",
			"cannot contain more than",
		},
		"reserved key prefix denied": {
			map[string]string{
				MetaKeyReservedPrefix + "key": "value1",
			},
			false,
			"reserved for internal use",
			"reserved for internal use",
			"reserved for internal use",
		},
		"reserved key prefix allowed": {
			map[string]string{
				MetaKeyReservedPrefix + "key": "value1",
			},
			true,
			"",
			"",
			"",
		},
		"reserved key prefix allowed via an allowlist just for gateway - " + MetaWANFederationKey: {
			map[string]string{
				MetaWANFederationKey: "value1",
			},
			false,
			"reserved for internal use",
			"reserved for internal use",
			"",
		},
	}

	for name, tc := range cases {
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Run("ValidateNodeMetadata", func(t *testing.T) {
				err := ValidateNodeMetadata(tc.Meta, tc.AllowConsulPrefix)
				if tc.NodeError == "" {
					require.NoError(t, err)
				} else {
					testutil.RequireErrorContains(t, err, tc.NodeError)
				}
			})
			t.Run("ValidateServiceMetadata - typical", func(t *testing.T) {
				err := ValidateServiceMetadata(ServiceKindTypical, tc.Meta, tc.AllowConsulPrefix)
				if tc.ServiceError == "" {
					require.NoError(t, err)
				} else {
					testutil.RequireErrorContains(t, err, tc.ServiceError)
				}
			})
			t.Run("ValidateServiceMetadata - mesh-gateway", func(t *testing.T) {
				err := ValidateServiceMetadata(ServiceKindMeshGateway, tc.Meta, tc.AllowConsulPrefix)
				if tc.GatewayError == "" {
					require.NoError(t, err)
				} else {
					testutil.RequireErrorContains(t, err, tc.GatewayError)
				}
			})
		})
	}
}

func TestStructs_validateMetaPair(t *testing.T) {
	longKey := strings.Repeat("a", metaKeyMaxLength+1)
	longValue := strings.Repeat("b", metaValueMaxLength+1)
	pairs := []struct {
		Key               string
		Value             string
		Error             string
		AllowConsulPrefix bool
		AllowConsulKeys   map[string]struct{}
	}{
		// valid pair
		{"key", "value", "", false, nil},
		// invalid, blank key
		{"", "value", "cannot be blank", false, nil},
		// allowed special chars in key name
		{"k_e-y", "value", "", false, nil},
		// disallowed special chars in key name
		{"(%key&)", "value", "invalid characters", false, nil},
		// key too long
		{longKey, "value", "Key is too long", false, nil},
		// reserved prefix
		{MetaKeyReservedPrefix + "key", "value", "reserved for internal use", false, nil},
		// reserved prefix, allowed
		{MetaKeyReservedPrefix + "key", "value", "", true, nil},
		// reserved prefix, not allowed via an allowlist
		{MetaKeyReservedPrefix + "bad", "value", "reserved for internal use", false, map[string]struct{}{MetaKeyReservedPrefix + "good": {}}},
		// reserved prefix, allowed via an allowlist
		{MetaKeyReservedPrefix + "good", "value", "", true, map[string]struct{}{MetaKeyReservedPrefix + "good": {}}},
		// value too long
		{"key", longValue, "Value is too long", false, nil},
	}

	for _, pair := range pairs {
		err := validateMetaPair(pair.Key, pair.Value, pair.AllowConsulPrefix, pair.AllowConsulKeys)
		if pair.Error == "" && err != nil {
			t.Fatalf("should have succeeded: %v, %v", pair, err)
		} else if pair.Error != "" && !strings.Contains(err.Error(), pair.Error) {
			t.Fatalf("should have failed: %v, %v", pair, err)
		}
	}
}

func TestDCSpecificRequest_CacheInfoKey(t *testing.T) {
	assertCacheInfoKeyIsComplete(t, &DCSpecificRequest{})
}

func TestNodeSpecificRequest_CacheInfoKey(t *testing.T) {
	assertCacheInfoKeyIsComplete(t, &NodeSpecificRequest{})
}

func TestServiceSpecificRequest_CacheInfoKey(t *testing.T) {
	assertCacheInfoKeyIsComplete(t, &ServiceSpecificRequest{})
}

func TestServiceDumpRequest_CacheInfoKey(t *testing.T) {
	// ServiceKind is only included when UseServiceKind=true
	assertCacheInfoKeyIsComplete(t, &ServiceDumpRequest{}, "ServiceKind")
}

// cacheInfoIgnoredFields are fields that can be ignored in all cache.Request types
// because the cache itself includes these values in the cache key, or because
// they are options used to specify the cache operation, and are not part of the
// cache entry value.
var cacheInfoIgnoredFields = map[string]bool{
	// Datacenter is part of the cache key added by the cache itself.
	"Datacenter": true,
	// PeerName is part of the cache key added by the cache itself.
	"PeerName": true,
	// QuerySource is always the same for every request from a single agent, so it
	// is excluded from the key.
	"Source": true,
	// EnterpriseMeta is an empty struct, so can not be included.
	enterpriseMetaField: true,
}

// assertCacheInfoKeyIsComplete is an assertion to verify that all fields on a request
// struct are considered as part of the cache key. It is used to prevent regressions
// when new fields are added to the struct. If a field is not included in the cache
// key it can lead to API requests or DNS requests returning the wrong value
// because a request matches the wrong entry in the agent/cache.Cache.
func assertCacheInfoKeyIsComplete(t *testing.T, request cache.Request, ignoredFields ...string) {
	t.Helper()

	ignored := make(map[string]bool, len(ignoredFields))
	for _, f := range ignoredFields {
		ignored[f] = true
	}

	fuzzer := fuzz.NewWithSeed(time.Now().UnixNano())
	fuzzer.Funcs(randQueryOptions)
	fuzzer.Fuzz(request)
	requestValue := reflect.ValueOf(request).Elem()

	for i := 0; i < requestValue.NumField(); i++ {
		originalKey := request.CacheInfo().Key
		field := requestValue.Field(i)
		fieldName := requestValue.Type().Field(i).Name
		originalValue := field.Interface()

		if cacheInfoIgnoredFields[fieldName] || ignored[fieldName] {
			continue
		}

		for i := 0; reflect.DeepEqual(originalValue, field.Interface()) && i < 20; i++ {
			fuzzer.Fuzz(field.Addr().Interface())
		}

		key := request.CacheInfo().Key
		if originalKey == key {
			t.Fatalf("expected field %v to be represented in the CacheInfo.Key, %v change to %v (key: %v)",
				fieldName,
				originalValue,
				field.Interface(),
				key)
		}
	}
}

func randQueryOptions(o *QueryOptions, c fuzz.Continue) {
	c.Fuzz(&o.Filter)
}

func TestSpecificServiceRequest_CacheInfo(t *testing.T) {
	tests := []struct {
		name     string
		req      ServiceSpecificRequest
		mutate   func(req *ServiceSpecificRequest)
		want     *cache.RequestInfo
		wantSame bool
	}{
		{
			name: "basic params",
			req: ServiceSpecificRequest{
				QueryOptions: QueryOptions{Token: "foo"},
				Datacenter:   "dc1",
			},
			want: &cache.RequestInfo{
				Token:      "foo",
				Datacenter: "dc1",
			},
			wantSame: true,
		},
		{
			name: "name should be considered",
			req: ServiceSpecificRequest{
				ServiceName: "web",
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.ServiceName = "db"
			},
			wantSame: false,
		},
		{
			name: "node meta should be considered",
			req: ServiceSpecificRequest{
				NodeMetaFilters: map[string]string{
					"foo": "bar",
				},
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.NodeMetaFilters = map[string]string{
					"foo": "qux",
				}
			},
			wantSame: false,
		},
		{
			name: "address should be considered",
			req: ServiceSpecificRequest{
				ServiceAddress: "1.2.3.4",
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.ServiceAddress = "4.3.2.1"
			},
			wantSame: false,
		},
		{
			name: "tag filter should be considered",
			req: ServiceSpecificRequest{
				TagFilter: true,
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.TagFilter = false
			},
			wantSame: false,
		},
		{
			name: "connect should be considered",
			req: ServiceSpecificRequest{
				Connect: true,
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.Connect = false
			},
			wantSame: false,
		},
		{
			name: "tags should be different",
			req: ServiceSpecificRequest{
				ServiceName: "web",
				ServiceTags: []string{"foo"},
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.ServiceTags = []string{"foo", "bar"}
			},
			wantSame: false,
		},
		{
			name: "tags should not depend on order",
			req: ServiceSpecificRequest{
				ServiceName: "web",
				ServiceTags: []string{"bar", "foo"},
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.ServiceTags = []string{"foo", "bar"}
			},
			wantSame: true,
		},
		// DEPRECATED (singular-service-tag) - remove this when upgrade RPC compat
		// with 1.2.x is not required.
		{
			name: "legacy requests with singular tag should be different",
			req: ServiceSpecificRequest{
				ServiceName: "web",
				ServiceTag:  "foo",
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.ServiceTag = "bar"
			},
			wantSame: false,
		},
		{
			name: "with integress=true",
			req: ServiceSpecificRequest{
				Datacenter:  "dc1",
				ServiceName: "my-service",
			},
			mutate: func(req *ServiceSpecificRequest) {
				req.Ingress = true
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			info := tc.req.CacheInfo()
			if tc.mutate != nil {
				tc.mutate(&tc.req)
			}
			afterInfo := tc.req.CacheInfo()

			// Check key matches or not
			if tc.wantSame {
				require.Equal(t, info, afterInfo)
			} else {
				require.NotEqual(t, info, afterInfo)
			}

			if tc.want != nil {
				// Reset key since we don't care about the actual hash value as long as
				// it does/doesn't change appropriately (asserted with wantSame above).
				info.Key = ""
				require.Equal(t, *tc.want, info)
			}
		})
	}
}

func TestNodeService_JSON_OmitTaggedAdddresses(t *testing.T) {
	cases := []struct {
		name string
		ns   NodeService
	}{
		{
			"nil",
			NodeService{
				TaggedAddresses: nil,
			},
		},
		{
			"empty",
			NodeService{
				TaggedAddresses: make(map[string]ServiceAddress),
			},
		},
	}

	for _, tc := range cases {
		name := tc.name
		ns := tc.ns
		t.Run(name, func(t *testing.T) {
			data, err := json.Marshal(ns)
			require.NoError(t, err)
			var raw map[string]interface{}
			err = json.Unmarshal(data, &raw)
			require.NoError(t, err)
			require.NotContains(t, raw, "TaggedAddresses")
			require.NotContains(t, raw, "tagged_addresses")
		})
	}
}

func TestServiceNode_JSON_OmitServiceTaggedAdddresses(t *testing.T) {
	cases := []struct {
		name string
		sn   ServiceNode
	}{
		{
			"nil",
			ServiceNode{
				ServiceTaggedAddresses: nil,
			},
		},
		{
			"empty",
			ServiceNode{
				ServiceTaggedAddresses: make(map[string]ServiceAddress),
			},
		},
	}

	for _, tc := range cases {
		name := tc.name
		sn := tc.sn
		t.Run(name, func(t *testing.T) {
			data, err := json.Marshal(sn)
			require.NoError(t, err)
			var raw map[string]interface{}
			err = json.Unmarshal(data, &raw)
			require.NoError(t, err)
			require.NotContains(t, raw, "ServiceTaggedAddresses")
			require.NotContains(t, raw, "service_tagged_addresses")
		})
	}
}

func TestNode_BestAddress(t *testing.T) {

	type testCase struct {
		input   Node
		lanAddr string
		wanAddr string
	}

	nodeAddr := "10.1.2.3"
	nodeWANAddr := "198.18.19.20"

	cases := map[string]testCase{
		"address": {
			input: Node{
				Address: nodeAddr,
			},

			lanAddr: nodeAddr,
			wanAddr: nodeAddr,
		},
		"wan-address": {
			input: Node{
				Address: nodeAddr,
				TaggedAddresses: map[string]string{
					"wan": nodeWANAddr,
				},
			},

			lanAddr: nodeAddr,
			wanAddr: nodeWANAddr,
		},
	}

	for name, tc := range cases {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {

			require.Equal(t, tc.lanAddr, tc.input.BestAddress(false))
			require.Equal(t, tc.wanAddr, tc.input.BestAddress(true))
		})
	}
}

func TestNodeService_BestAddress(t *testing.T) {

	type testCase struct {
		input   NodeService
		lanAddr string
		lanPort int
		wanAddr string
		wanPort int
	}

	serviceAddr := "10.2.3.4"
	servicePort := 1234
	serviceWANAddr := "198.19.20.21"
	serviceWANPort := 987

	cases := map[string]testCase{
		"no-address": {
			input: NodeService{
				Port: servicePort,
			},

			lanAddr: "",
			lanPort: servicePort,
			wanAddr: "",
			wanPort: servicePort,
		},
		"service-address": {
			input: NodeService{
				Address: serviceAddr,
				Port:    servicePort,
			},

			lanAddr: serviceAddr,
			lanPort: servicePort,
			wanAddr: serviceAddr,
			wanPort: servicePort,
		},
		"service-wan-address": {
			input: NodeService{
				Address: serviceAddr,
				Port:    servicePort,
				TaggedAddresses: map[string]ServiceAddress{
					"wan": {
						Address: serviceWANAddr,
						Port:    serviceWANPort,
					},
				},
			},

			lanAddr: serviceAddr,
			lanPort: servicePort,
			wanAddr: serviceWANAddr,
			wanPort: serviceWANPort,
		},
		"service-wan-address-default-port": {
			input: NodeService{
				Address: serviceAddr,
				Port:    servicePort,
				TaggedAddresses: map[string]ServiceAddress{
					"wan": {
						Address: serviceWANAddr,
						Port:    0,
					},
				},
			},

			lanAddr: serviceAddr,
			lanPort: servicePort,
			wanAddr: serviceWANAddr,
			wanPort: servicePort,
		},
		"service-wan-address-node-lan": {
			input: NodeService{
				Port: servicePort,
				TaggedAddresses: map[string]ServiceAddress{
					"wan": {
						Address: serviceWANAddr,
						Port:    serviceWANPort,
					},
				},
			},

			lanAddr: "",
			lanPort: servicePort,
			wanAddr: serviceWANAddr,
			wanPort: serviceWANPort,
		},
	}

	for name, tc := range cases {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {

			addr, port := tc.input.BestAddress(false)
			require.Equal(t, tc.lanAddr, addr)
			require.Equal(t, tc.lanPort, port)

			addr, port = tc.input.BestAddress(true)
			require.Equal(t, tc.wanAddr, addr)
			require.Equal(t, tc.wanPort, port)
		})
	}
}

func TestCheckServiceNode_BestAddress(t *testing.T) {

	type testCase struct {
		input   CheckServiceNode
		lanAddr string
		lanPort int
		lanIdx  uint64
		wanAddr string
		wanPort int
		wanIdx  uint64
	}

	nodeAddr := "10.1.2.3"
	nodeWANAddr := "198.18.19.20"
	nodeIdx := uint64(11)
	serviceAddr := "10.2.3.4"
	servicePort := 1234
	serviceIdx := uint64(22)
	serviceWANAddr := "198.19.20.21"
	serviceWANPort := 987

	cases := map[string]testCase{
		"node-address": {
			input: CheckServiceNode{
				Node: &Node{
					Address: nodeAddr,
					RaftIndex: RaftIndex{
						ModifyIndex: nodeIdx,
					},
				},
				Service: &NodeService{
					Port: servicePort,
					RaftIndex: RaftIndex{
						ModifyIndex: serviceIdx,
					},
				},
			},

			lanAddr: nodeAddr,
			lanIdx:  nodeIdx,
			lanPort: servicePort,
			wanAddr: nodeAddr,
			wanIdx:  nodeIdx,
			wanPort: servicePort,
		},
		"node-wan-address": {
			input: CheckServiceNode{
				Node: &Node{
					Address: nodeAddr,
					TaggedAddresses: map[string]string{
						"wan": nodeWANAddr,
					},
					RaftIndex: RaftIndex{
						ModifyIndex: nodeIdx,
					},
				},
				Service: &NodeService{
					Port: servicePort,
					RaftIndex: RaftIndex{
						ModifyIndex: serviceIdx,
					},
				},
			},

			lanAddr: nodeAddr,
			lanIdx:  nodeIdx,
			lanPort: servicePort,
			wanAddr: nodeWANAddr,
			wanIdx:  nodeIdx,
			wanPort: servicePort,
		},
		"service-address": {
			input: CheckServiceNode{
				Node: &Node{
					Address: nodeAddr,
					// this will be ignored
					TaggedAddresses: map[string]string{
						"wan": nodeWANAddr,
					},
					RaftIndex: RaftIndex{
						ModifyIndex: nodeIdx,
					},
				},
				Service: &NodeService{
					Address: serviceAddr,
					Port:    servicePort,
					RaftIndex: RaftIndex{
						ModifyIndex: serviceIdx,
					},
				},
			},

			lanAddr: serviceAddr,
			lanIdx:  serviceIdx,
			lanPort: servicePort,
			wanAddr: serviceAddr,
			wanIdx:  serviceIdx,
			wanPort: servicePort,
		},
		"service-wan-address": {
			input: CheckServiceNode{
				Node: &Node{
					Address: nodeAddr,
					// this will be ignored
					TaggedAddresses: map[string]string{
						"wan": nodeWANAddr,
					},
					RaftIndex: RaftIndex{
						ModifyIndex: nodeIdx,
					},
				},
				Service: &NodeService{
					Address: serviceAddr,
					Port:    servicePort,
					TaggedAddresses: map[string]ServiceAddress{
						"wan": {
							Address: serviceWANAddr,
							Port:    serviceWANPort,
						},
					},
					RaftIndex: RaftIndex{
						ModifyIndex: serviceIdx,
					},
				},
			},

			lanAddr: serviceAddr,
			lanIdx:  serviceIdx,
			lanPort: servicePort,
			wanAddr: serviceWANAddr,
			wanIdx:  serviceIdx,
			wanPort: serviceWANPort,
		},
		"service-wan-address-default-port": {
			input: CheckServiceNode{
				Node: &Node{
					Address: nodeAddr,
					// this will be ignored
					TaggedAddresses: map[string]string{
						"wan": nodeWANAddr,
					},
					RaftIndex: RaftIndex{
						ModifyIndex: nodeIdx,
					},
				},
				Service: &NodeService{
					Address: serviceAddr,
					Port:    servicePort,
					TaggedAddresses: map[string]ServiceAddress{
						"wan": {
							Address: serviceWANAddr,
							Port:    0,
						},
					},
					RaftIndex: RaftIndex{
						ModifyIndex: serviceIdx,
					},
				},
			},

			lanAddr: serviceAddr,
			lanIdx:  serviceIdx,
			lanPort: servicePort,
			wanAddr: serviceWANAddr,
			wanIdx:  serviceIdx,
			wanPort: servicePort,
		},
		"service-wan-address-node-lan": {
			input: CheckServiceNode{
				Node: &Node{
					Address: nodeAddr,
					// this will be ignored
					TaggedAddresses: map[string]string{
						"wan": nodeWANAddr,
					},
					RaftIndex: RaftIndex{
						ModifyIndex: nodeIdx,
					},
				},
				Service: &NodeService{
					Port: servicePort,
					TaggedAddresses: map[string]ServiceAddress{
						"wan": {
							Address: serviceWANAddr,
							Port:    serviceWANPort,
						},
					},
					RaftIndex: RaftIndex{
						ModifyIndex: serviceIdx,
					},
				},
			},

			lanAddr: nodeAddr,
			lanIdx:  nodeIdx,
			lanPort: servicePort,
			wanAddr: serviceWANAddr,
			wanIdx:  serviceIdx,
			wanPort: serviceWANPort,
		},
	}

	for name, tc := range cases {
		name := name
		tc := tc
		t.Run(name, func(t *testing.T) {

			idx, addr, port := tc.input.BestAddress(false)
			require.Equal(t, tc.lanAddr, addr)
			require.Equal(t, tc.lanPort, port)
			require.Equal(t, tc.lanIdx, idx)

			idx, addr, port = tc.input.BestAddress(true)
			require.Equal(t, tc.wanAddr, addr)
			require.Equal(t, tc.wanPort, port)
			require.Equal(t, tc.wanIdx, idx)
		})
	}
}

func TestNodeService_JSON_Marshal(t *testing.T) {
	ns := &NodeService{
		Service: "foo",
		Proxy: ConnectProxyConfig{
			Config: map[string]interface{}{
				"bind_addresses": map[string]interface{}{
					"default": map[string]interface{}{
						"Address": "0.0.0.0",
						"Port":    "443",
					},
				},
			},
		},
	}
	buf, err := json.Marshal(ns)
	require.NoError(t, err)

	var out NodeService
	require.NoError(t, json.Unmarshal(buf, &out))
	require.Equal(t, *ns, out)
}

func TestServiceNode_JSON_Marshal(t *testing.T) {
	sn := &ServiceNode{
		Node:        "foo",
		ServiceName: "foo",
		ServiceProxy: ConnectProxyConfig{
			Config: map[string]interface{}{
				"bind_addresses": map[string]interface{}{
					"default": map[string]interface{}{
						"Address": "0.0.0.0",
						"Port":    "443",
					},
				},
			},
		},
	}
	buf, err := json.Marshal(sn)
	require.NoError(t, err)

	var out ServiceNode
	require.NoError(t, json.Unmarshal(buf, &out))
	require.Equal(t, *sn, out)
}

// frankensteinStruct is an amalgamation of all of the different kinds of
// fields you could have on struct defined in the agent/structs package that we
// send through msgpack
type frankensteinStruct struct {
	Child      *monsterStruct
	ChildSlice []*monsterStruct
	ChildMap   map[string]*monsterStruct
}
type monsterStruct struct {
	Bool    bool
	Int     int
	Uint8   uint8
	Uint64  uint64
	Float32 float32
	Float64 float64
	String  string

	Hash         []byte
	Uint32Slice  []uint32
	Float64Slice []float64
	StringSlice  []string

	MapInt         map[string]int
	MapString      map[string]string
	MapStringSlice map[string][]string

	// We explicitly DO NOT try to test the following types that involve
	// interface{} as the TestMsgpackEncodeDecode test WILL fail.
	//
	// These are tested elsewhere for the very specific scenario in question,
	// which usually takes a secondary trip through mapstructure during decode
	// which papers over some of the additional conversions necessary to finish
	// decoding.
	// MapIface    map[string]interface{}
	// MapMapIface map[string]map[string]interface{}

	Dur     time.Duration
	DurPtr  *time.Duration
	Time    time.Time
	TimePtr *time.Time

	RaftIndex
}

func makeFrank() *frankensteinStruct {
	return &frankensteinStruct{
		Child: makeMonster(),
		ChildSlice: []*monsterStruct{
			makeMonster(),
			makeMonster(),
		},
		ChildMap: map[string]*monsterStruct{
			"one": makeMonster(), // only put one key in here so the map order is fixed
		},
	}
}

func makeMonster() *monsterStruct {
	var d = 9 * time.Hour
	var t = time.Date(2008, 1, 2, 3, 4, 5, 0, time.UTC)

	return &monsterStruct{
		Bool:    true,
		Int:     -8,
		Uint8:   5,
		Uint64:  9,
		Float32: 5.25,
		Float64: 99.5,
		String:  "strval",

		Hash:         []byte("hello"),
		Uint32Slice:  []uint32{1, 2, 3, 4},
		Float64Slice: []float64{9.2, 6.25},
		StringSlice:  []string{"foo", "bar"},

		// // MapIface will hold an amalgam of what AuthMethods and
		// // CAConfigurations use in 'Config'
		// MapIface: map[string]interface{}{
		// 	"Name":  "inner",
		// 	"Dur":   "5s",
		// 	"Bool":  true,
		// 	"Float": 15.25,
		// 	"Int":   int64(94),
		// 	"Nested": map[string]string{ // this doesn't survive
		// 		"foo": "bar",
		// 	},
		// },
		// // MapMapIface    map[string]map[string]interface{}

		MapInt: map[string]int{
			"int": 5,
		},
		MapString: map[string]string{
			"aaa": "bbb",
		},
		MapStringSlice: map[string][]string{
			"aaa": {"bbb"},
		},

		Dur:     5 * time.Second,
		DurPtr:  &d,
		Time:    t.Add(-5 * time.Hour),
		TimePtr: &t,

		RaftIndex: RaftIndex{
			CreateIndex: 1,
			ModifyIndex: 3,
		},
	}
}

func TestStructs_MsgpackEncodeDecode_Monolith(t *testing.T) {
	t.Run("monster", func(t *testing.T) {
		in := makeMonster()
		TestMsgpackEncodeDecode(t, in, false)
	})
	t.Run("frankenstein", func(t *testing.T) {
		in := makeFrank()
		TestMsgpackEncodeDecode(t, in, false)
	})
}

func TestSnapshotRequestResponse_MsgpackEncodeDecode(t *testing.T) {
	t.Run("request", func(t *testing.T) {
		in := &SnapshotRequest{
			Datacenter: "foo",
			Token:      "blah",
			AllowStale: true,
			Op:         SnapshotRestore,
		}
		TestMsgpackEncodeDecode(t, in, true)
	})
	t.Run("response", func(t *testing.T) {
		in := &SnapshotResponse{
			Error: "blah",
			QueryMeta: QueryMeta{
				Index:                 3,
				LastContact:           5 * time.Second,
				KnownLeader:           true,
				ConsistencyLevel:      "default",
				ResultsFilteredByACLs: true,
			},
		}
		TestMsgpackEncodeDecode(t, in, true)
	})

}

func TestGatewayService_IsSame(t *testing.T) {
	gateway := NewServiceName("gateway", nil)
	svc := NewServiceName("web", nil)
	kind := ServiceKindTerminatingGateway
	ca := "ca.pem"
	cert := "client.pem"
	key := "tls.key"
	sni := "mydomain"
	wildcard := false

	g := &GatewayService{
		Gateway:      gateway,
		Service:      svc,
		GatewayKind:  kind,
		CAFile:       ca,
		CertFile:     cert,
		KeyFile:      key,
		SNI:          sni,
		FromWildcard: wildcard,
	}
	other := &GatewayService{
		Gateway:      gateway,
		Service:      svc,
		GatewayKind:  kind,
		CAFile:       ca,
		CertFile:     cert,
		KeyFile:      key,
		SNI:          sni,
		FromWildcard: wildcard,
	}
	check := func(twiddle, restore func()) {
		t.Helper()
		if !g.IsSame(other) || !other.IsSame(g) {
			t.Fatalf("should be the same")
		}

		twiddle()
		if g.IsSame(other) || other.IsSame(g) {
			t.Fatalf("should be different, was %#v VS %#v", g, other)
		}

		restore()
		if !g.IsSame(other) || !other.IsSame(g) {
			t.Fatalf("should be the same")
		}
	}
	check(func() { other.Gateway = NewServiceName("other", nil) }, func() { other.Gateway = gateway })
	check(func() { other.Service = NewServiceName("other", nil) }, func() { other.Service = svc })
	check(func() { other.GatewayKind = ServiceKindIngressGateway }, func() { other.GatewayKind = kind })
	check(func() { other.CAFile = "/certs/cert.pem" }, func() { other.CAFile = ca })
	check(func() { other.CertFile = "/certs/cert.pem" }, func() { other.CertFile = cert })
	check(func() { other.KeyFile = "/certs/cert.pem" }, func() { other.KeyFile = key })
	check(func() { other.SNI = "alt-domain" }, func() { other.SNI = sni })
	check(func() { other.FromWildcard = true }, func() { other.FromWildcard = wildcard })

	if !g.IsSame(other) {
		t.Fatalf("should be equal, was %#v VS %#v", g, other)
	}
}

func TestServiceList_Sort(t *testing.T) {
	type testcase struct {
		name   string
		list   []ServiceName
		expect []ServiceName
	}

	run := func(t *testing.T, tc testcase) {
		t.Run("written order", func(t *testing.T) {
			ServiceList(tc.list).Sort()
			require.Equal(t, tc.expect, tc.list)
		})
		t.Run("random order", func(t *testing.T) {
			rand.Shuffle(len(tc.list), func(i, j int) {
				tc.list[i], tc.list[j] = tc.list[j], tc.list[i]
			})
			ServiceList(tc.list).Sort()
			require.Equal(t, tc.expect, tc.list)
		})
	}

	sn := func(name string) ServiceName {
		return NewServiceName(name, nil)
	}

	cases := []testcase{
		{
			name:   "nil",
			list:   nil,
			expect: nil,
		},
		{
			name:   "empty",
			list:   []ServiceName{},
			expect: []ServiceName{},
		},
		{
			name:   "one",
			list:   []ServiceName{sn("foo")},
			expect: []ServiceName{sn("foo")},
		},
		{
			name: "multiple",
			list: []ServiceName{
				sn("food"),
				sn("zip"),
				sn("Bar"),
				sn("ba"),
				sn("foo"),
				sn("bar"),
				sn("Foo"),
				sn("Zip"),
				sn("foo"),
				sn("bar"),
				sn("barrier"),
			},
			expect: []ServiceName{
				sn("Bar"),
				sn("Foo"),
				sn("Zip"),
				sn("ba"),
				sn("bar"),
				sn("bar"),
				sn("barrier"),
				sn("foo"),
				sn("foo"),
				sn("food"),
				sn("zip"),
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

func TestServicePorts_Validate(t *testing.T) {
	type testCase struct {
		ports       ServicePorts
		expectedErr string
	}

	cases := map[string]testCase{
		"valid": {
			ports: ServicePorts{
				{
					Name:    "http",
					Port:    80,
					Default: true,
				},
				{
					Name: "https",
					Port: 443,
				},
			},
			expectedErr: "",
		},
		"empty": {
			ports:       ServicePorts{},
			expectedErr: "",
		},
		"invalid_duplicate_name": {
			ports: ServicePorts{
				{
					Name:    "http",
					Port:    80,
					Default: true,
				},
				{
					Name: "http",
					Port: 8080,
				},
			},
			expectedErr: "Ports.Name \"http\" has to be unique",
		},
		"invalid_port_name": {
			ports: ServicePorts{
				{
					Name: "",
					Port: 80,
				},
			},
			expectedErr: "Ports.Name cannot be empty",
		},
		"invalid_port_number": {
			ports: ServicePorts{
				{
					Name: "http",
					Port: 0,
				},
			},
			expectedErr: "Ports.Port must be greater than zero",
		},
		"invalid_negative_port_number": {
			ports: ServicePorts{
				{
					Name: "http",
					Port: -8080,
				},
			},
			expectedErr: "Ports.Port must be greater than zero",
		},
		"invalid_no_default": {
			ports: ServicePorts{
				{
					Name: "http",
					Port: 80,
				},
			},
			expectedErr: "One of the Ports must be marked as Default",
		},
		"invalid_multiple_default": {
			ports: ServicePorts{
				{
					Name:    "http",
					Port:    80,
					Default: true,
				},
				{
					Name:    "https",
					Port:    443,
					Default: true,
				},
			},
			expectedErr: "Only one port can be marked as default",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := tc.ports.Validate()
			if tc.expectedErr == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.expectedErr)
			}
		})
	}

}

func TestServicePorts_GetPortWithName(t *testing.T) {
	ports := ServicePorts{
		{
			Name:    "http",
			Port:    80,
			Default: true,
		},
		{
			Name: "https",
			Port: 443,
		},
	}

	p, ok := ports.GetPortWithName("http")
	require.True(t, ok)
	require.Equal(t, 80, p)

	p, ok = ports.GetPortWithName("https")
	require.True(t, ok)
	require.Equal(t, 443, p)

	p, ok = ports.GetPortWithName("other")
	require.False(t, ok)
	require.Equal(t, 0, p)
}
