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

package config

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"net"
	"net/netip"
	"os"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/armon/go-metrics/prometheus"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/require"
	"golang.org/x/time/rate"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/cache"
	"github.com/hashicorp/consul/agent/checks"
	"github.com/hashicorp/consul/agent/consul"
	consulrate "github.com/hashicorp/consul/agent/consul/rate"
	hcpconfig "github.com/hashicorp/consul/agent/hcp/config"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/agent/token"
	"github.com/hashicorp/consul/lib"
	"github.com/hashicorp/consul/logging"
	"github.com/hashicorp/consul/proto/private/prototest"
	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/tlsutil"
	"github.com/hashicorp/consul/types"
)

// testCase used to test different config loading and flag parsing scenarios.
type testCase struct {
	desc             string
	args             []string
	setup            func() // TODO: accept a testing.T instead of panic
	cleanup          func()
	expected         func(rt *RuntimeConfig)
	expectedErr      string
	expectedWarnings []string
	opts             LoadOpts
	json             []string
	hcl              []string
}

func (tc testCase) source(format string) []string {
	if format == "hcl" {
		return tc.hcl
	}
	return tc.json
}

var defaultGrpcTlsAddr = net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8503"))

// TestConfigFlagsAndEdgecases tests the command line flags and
// edgecases for the config parsing. It provides a test structure which
// checks for warnings on deprecated fields and flags.  These tests
// should check one option at a time if possible
func TestLoad_IntegrationWithFlags(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}
	dataDir := testutil.TempDir(t, "config")

	run := func(t *testing.T, tc testCase) {
		t.Helper()
		if len(tc.json) == 0 && len(tc.hcl) == 0 {
			runCase(t, tc.desc, tc.run("", dataDir))
			return
		}

		for _, format := range []string{"json", "hcl"} {
			name := fmt.Sprintf("%v_%v", tc.desc, format)
			runCase(t, name, tc.run(format, dataDir))
		}
	}

	defaultEntMeta := structs.DefaultEnterpriseMetaInDefaultPartition()

	// ------------------------------------------------------------
	// cmd line flags
	//

	run(t, testCase{
		desc: "-advertise",
		args: []string{
			`-advertise=1.2.3.4`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("1.2.3.4")
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.2.3.4",
				"lan_ipv4": "1.2.3.4",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-advertise-wan",
		args: []string{
			`-advertise-wan=1.2.3.4`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "10.0.0.1",
				"lan_ipv4": "10.0.0.1",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-advertise and -advertise-wan",
		args: []string{
			`-advertise=1.2.3.4`,
			`-advertise-wan=5.6.7.8`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("1.2.3.4")
			rt.AdvertiseAddrWAN = ipAddr("5.6.7.8")
			rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("5.6.7.8:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.2.3.4",
				"lan_ipv4": "1.2.3.4",
				"wan":      "5.6.7.8",
				"wan_ipv4": "5.6.7.8",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-bind",
		args: []string{
			`-bind=1.2.3.4`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.BindAddr = ipAddr("1.2.3.4")
			rt.AdvertiseAddrLAN = ipAddr("1.2.3.4")
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300")
			rt.RPCBindAddr = tcpAddr("1.2.3.4:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.SerfBindAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.SerfBindAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.2.3.4",
				"lan_ipv4": "1.2.3.4",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-bootstrap",
		args: []string{
			`-bootstrap`,
			`-server`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Bootstrap = true
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.DataDir = dataDir
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
		expectedWarnings: []string{"bootstrap = true: do not enable unless necessary"},
	})
	run(t, testCase{
		desc: "-bootstrap-expect",
		args: []string{
			`-bootstrap-expect=3`,
			`-server`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.BootstrapExpect = 3
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.DataDir = dataDir
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
		expectedWarnings: []string{"bootstrap_expect > 0: expecting 3 servers"},
	})
	run(t, testCase{
		desc: "-client",
		args: []string{
			`-client=1.2.3.4`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.TLS.ServerMode = false
			rt.ClientAddrs = []*net.IPAddr{ipAddr("1.2.3.4")}
			rt.DNSAddrs = []net.Addr{tcpAddr("1.2.3.4:8600"), udpAddr("1.2.3.4:8600")}
			rt.HTTPAddrs = []net.Addr{tcpAddr("1.2.3.4:8500")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-config-dir",
		args: []string{
			`-data-dir=` + dataDir,
			`-config-dir`, filepath.Join(dataDir, "conf.d"),
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
		setup: func() {
			writeFile(filepath.Join(dataDir, "conf.d/conf.json"), []byte(`{"datacenter":"a"}`))
		},
	})
	run(t, testCase{
		desc: "-config-file json",
		args: []string{
			`-data-dir=` + dataDir,
			`-config-file`, filepath.Join(dataDir, "conf.json"),
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
		setup: func() {
			writeFile(filepath.Join(dataDir, "conf.json"), []byte(`{"datacenter":"a"}`))
		},
	})
	run(t, testCase{
		desc: "-config-file hcl and json",
		args: []string{
			`-data-dir=` + dataDir,
			`-config-file`, filepath.Join(dataDir, "conf.hcl"),
			`-config-file`, filepath.Join(dataDir, "conf.json"),
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "b"
			rt.PrimaryDatacenter = "b"
			rt.DataDir = dataDir
		},
		setup: func() {
			writeFile(filepath.Join(dataDir, "conf.hcl"), []byte(`datacenter = "a"`))
			writeFile(filepath.Join(dataDir, "conf.json"), []byte(`{"datacenter":"b"}`))
		},
	})
	run(t, testCase{
		desc: "-data-dir empty",
		args: []string{
			`-data-dir=`,
		},
		expectedErr: "data_dir cannot be empty",
	})
	run(t, testCase{
		desc: "-data-dir non-directory",
		args: []string{
			`-data-dir=runtime_test.go`,
		},
		expectedErr: `data_dir "runtime_test.go" is not a directory`,
	})
	run(t, testCase{
		desc: "-datacenter",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-datacenter empty",
		args: []string{
			`-datacenter=`,
			`-data-dir=` + dataDir,
		},
		expectedErr: "datacenter cannot be empty",
	})
	run(t, testCase{
		desc: "-dev",
		args: []string{
			`-dev`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("127.0.0.1")
			rt.AdvertiseAddrWAN = ipAddr("127.0.0.1")
			rt.BindAddr = ipAddr("127.0.0.1")
			rt.ConnectEnabled = true
			rt.DevMode = true
			rt.DisableAnonymousSignature = true
			rt.DisableKeyringFile = true
			rt.Experiments = nil
			rt.EnableDebug = true
			rt.UIConfig.Enabled = true
			rt.LeaveOnTerm = false
			rt.Logging.LogLevel = "DEBUG"
			rt.RPCAdvertiseAddr = tcpAddr("127.0.0.1:8300")
			rt.RPCBindAddr = tcpAddr("127.0.0.1:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("127.0.0.1:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("127.0.0.1:8302")
			rt.SerfBindAddrLAN = tcpAddr("127.0.0.1:8301")
			rt.SerfBindAddrWAN = tcpAddr("127.0.0.1:8302")
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.SkipLeaveOnInt = true
			rt.TaggedAddresses = map[string]string{
				"lan":      "127.0.0.1",
				"lan_ipv4": "127.0.0.1",
				"wan":      "127.0.0.1",
				"wan_ipv4": "127.0.0.1",
			}
			rt.ConsulCoordinateUpdatePeriod = 100 * time.Millisecond
			rt.ConsulRaftElectionTimeout = 52 * time.Millisecond
			rt.ConsulRaftHeartbeatTimeout = 35 * time.Millisecond
			rt.ConsulRaftLeaderLeaseTimeout = 20 * time.Millisecond
			rt.GossipLANGossipInterval = 100 * time.Millisecond
			rt.GossipLANProbeInterval = 100 * time.Millisecond
			rt.GossipLANProbeTimeout = 100 * time.Millisecond
			rt.GossipLANSuspicionMult = 3
			rt.GossipWANGossipInterval = 100 * time.Millisecond
			rt.GossipWANProbeInterval = 100 * time.Millisecond
			rt.GossipWANProbeTimeout = 100 * time.Millisecond
			rt.GossipWANSuspicionMult = 3
			rt.ConsulServerHealthInterval = 10 * time.Millisecond
			rt.GRPCPort = 8502
			rt.GRPCAddrs = []net.Addr{tcpAddr("127.0.0.1:8502")}
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "-disable-host-node-id",
		args: []string{
			`-disable-host-node-id`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DisableHostNodeID = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-disable-keyring-file",
		args: []string{
			`-disable-keyring-file`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DisableKeyringFile = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-dns-port",
		args: []string{
			`-dns-port=123`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DNSPort = 123
			rt.DNSAddrs = []net.Addr{tcpAddr("127.0.0.1:123"), udpAddr("127.0.0.1:123")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-domain",
		args: []string{
			`-domain=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DNSDomain = "a"
			rt.TLS.Domain = "a"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-alt-domain",
		args: []string{
			`-alt-domain=alt`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DNSAltDomain = "alt"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-alt-domain can't be prefixed by DC",
		args: []string{
			`-datacenter=a`,
			`-alt-domain=a.alt`,
			`-data-dir=` + dataDir,
		},
		expectedErr: "alt_domain cannot start with {service,connect,node,query,addr,a}",
	})
	run(t, testCase{
		desc: "-alt-domain can't be prefixed by service",
		args: []string{
			`-alt-domain=service.alt`,
			`-data-dir=` + dataDir,
		},
		expectedErr: "alt_domain cannot start with {service,connect,node,query,addr,dc1}",
	})
	run(t, testCase{
		desc: "-alt-domain can be prefixed by non-keywords",
		args: []string{
			`-alt-domain=mydomain.alt`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DNSAltDomain = "mydomain.alt"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-alt-domain can't be prefixed by DC",
		args: []string{
			`-alt-domain=dc1.alt`,
			`-data-dir=` + dataDir,
		},
		expectedErr: "alt_domain cannot start with {service,connect,node,query,addr,dc1}",
	})
	run(t, testCase{
		desc: "-enable-script-checks",
		args: []string{
			`-enable-script-checks`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.EnableLocalScriptChecks = true
			rt.EnableRemoteScriptChecks = true
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{remoteScriptCheckSecurityWarning},
	})
	run(t, testCase{
		desc: "-encrypt",
		args: []string{
			`-encrypt=pUqJrVyVRj5jsiYEkM/tFQYfWyJIv4s3XkvDwy7Cu5s=`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.EncryptKey = "pUqJrVyVRj5jsiYEkM/tFQYfWyJIv4s3XkvDwy7Cu5s="
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-config-format disabled, skip unknown files",
		args: []string{
			`-data-dir=` + dataDir,
			`-config-dir`, filepath.Join(dataDir, "conf"),
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
		setup: func() {
			writeFile(filepath.Join(dataDir, "conf", "valid.json"), []byte(`{"datacenter":"a"}`))
			writeFile(filepath.Join(dataDir, "conf", "invalid.skip"), []byte(`NOPE`))
		},
		expectedWarnings: []string{
			"skipping file " + filepath.Join(dataDir, "conf", "invalid.skip") + ", extension must be .hcl or .json, or config format must be set",
		},
	})
	run(t, testCase{
		desc: "-config-format=json",
		args: []string{
			`-data-dir=` + dataDir,
			`-config-format=json`,
			`-config-file`, filepath.Join(dataDir, "conf"),
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
		setup: func() {
			writeFile(filepath.Join(dataDir, "conf"), []byte(`{"datacenter":"a"}`))
		},
	})
	run(t, testCase{
		desc: "-config-format=hcl",
		args: []string{
			`-data-dir=` + dataDir,
			`-config-format=hcl`,
			`-config-file`, filepath.Join(dataDir, "conf"),
		},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
		setup: func() {
			writeFile(filepath.Join(dataDir, "conf"), []byte(`datacenter = "a"`))
		},
	})
	run(t, testCase{
		desc: "-http-port",
		args: []string{
			`-http-port=123`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.HTTPPort = 123
			rt.HTTPAddrs = []net.Addr{tcpAddr("127.0.0.1:123")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-join",
		args: []string{
			`-join=a`,
			`-join=b`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinLAN = []string{"a", "b"}
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{
			deprecatedFlagWarning("-join", "-retry-join"),
		},
	})
	run(t, testCase{
		desc: "-join-wan",
		args: []string{
			`-join-wan=a`,
			`-join-wan=b`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinWAN = []string{"a", "b"}
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{
			deprecatedFlagWarning("-join-wan", "-retry-join-wan"),
		},
	})
	run(t, testCase{
		desc: "-log-level",
		args: []string{
			`-log-level=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Logging.LogLevel = "a"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-log-json",
		args: []string{
			`-log-json`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Logging.LogJSON = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-log-rotate-max-files",
		args: []string{
			`-log-rotate-max-files=2`,
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "log_rotate_max_files": 2 }`},
		hcl:  []string{`log_rotate_max_files = 2`},
		expected: func(rt *RuntimeConfig) {
			rt.Logging.LogRotateMaxFiles = 2
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-node",
		args: []string{
			`-node=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.NodeName = "a"
			rt.TLS.NodeName = "a"
			rt.DataDir = dataDir
			rt.Cloud.NodeName = "a"
		},
	})
	run(t, testCase{
		desc: "-node-id",
		args: []string{
			`-node-id=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.NodeID = "a"
			rt.DataDir = dataDir
			rt.Cloud.NodeID = "a"
		},
	})
	run(t, testCase{
		desc: "-node-meta",
		args: []string{
			`-node-meta=a:b`,
			`-node-meta=c:d`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.NodeMeta = map[string]string{"a": "b", "c": "d"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-non-voting-server",
		args: []string{
			`-non-voting-server`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.ReadReplica = true
			rt.DataDir = dataDir
		},
		expectedWarnings: enterpriseReadReplicaWarnings,
	})
	run(t, testCase{
		desc: "-pid-file",
		args: []string{
			`-pid-file=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.PidFile = "a"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-primary-gateway",
		args: []string{
			`-server`,
			`-datacenter=dc2`,
			`-primary-gateway=a`,
			`-primary-gateway=b`,
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "primary_datacenter": "dc1" }`},
		hcl:  []string{`primary_datacenter = "dc1"`},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "dc2"
			rt.PrimaryDatacenter = "dc1"
			rt.PrimaryGateways = []string{"a", "b"}
			rt.DataDir = dataDir
			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "-protocol",
		args: []string{
			`-protocol=1`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RPCProtocol = 1
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-raft-protocol",
		args: []string{
			`-raft-protocol=3`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RaftProtocol = 3
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-raft-protocol unsupported",
		args: []string{
			`-raft-protocol=2`,
			`-data-dir=` + dataDir,
		},
		expectedErr: "raft_protocol version 2 is not supported by this version of Consul",
	})
	run(t, testCase{
		desc: "-recursor",
		args: []string{
			`-recursor=1.2.3.4`,
			`-recursor=5.6.7.8`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DNSRecursors = []string{"1.2.3.4", "5.6.7.8"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-rejoin",
		args: []string{
			`-rejoin`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RejoinAfterLeave = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-retry-interval",
		args: []string{
			`-retry-interval=5s`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinIntervalLAN = 5 * time.Second
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-retry-interval-wan",
		args: []string{
			`-retry-interval-wan=5s`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinIntervalWAN = 5 * time.Second
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-retry-join",
		args: []string{
			`-retry-join=a`,
			`-retry-join=b`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinLAN = []string{"a", "b"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-retry-join-wan",
		args: []string{
			`-retry-join-wan=a`,
			`-retry-join-wan=b`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinWAN = []string{"a", "b"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-retry-max",
		args: []string{
			`-retry-max=1`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinMaxAttemptsLAN = 1
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-retry-max-wan",
		args: []string{
			`-retry-max-wan=1`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinMaxAttemptsWAN = 1
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-serf-lan-bind",
		args: []string{
			`-serf-lan-bind=1.2.3.4`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.SerfBindAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-serf-lan-port",
		args: []string{
			`-serf-lan-port=123`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.SerfPortLAN = 123
			rt.SerfAdvertiseAddrLAN = tcpAddr("10.0.0.1:123")
			rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:123")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-serf-wan-bind",
		args: []string{
			`-serf-wan-bind=1.2.3.4`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.SerfBindAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-serf-wan-port",
		args: []string{
			`-serf-wan-port=123`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.SerfPortWAN = 123
			rt.SerfAdvertiseAddrWAN = tcpAddr("10.0.0.1:123")
			rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:123")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-server",
		args: []string{
			`-server`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.DataDir = dataDir
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "-server-port",
		args: []string{
			`-server-port=123`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.ServerPort = 123
			rt.RPCAdvertiseAddr = tcpAddr("10.0.0.1:123")
			rt.RPCBindAddr = tcpAddr("0.0.0.0:123")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-syslog",
		args: []string{
			`-syslog`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Logging.EnableSyslog = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-ui",
		args: []string{
			`-ui`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.UIConfig.Enabled = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-ui-dir",
		args: []string{
			`-ui-dir=a`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.UIConfig.Dir = "a"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "-ui-content-path",
		args: []string{
			`-ui-content-path=/a/b`,
			`-data-dir=` + dataDir,
		},

		expected: func(rt *RuntimeConfig) {
			rt.UIConfig.ContentPath = "/a/b/"
			rt.DataDir = dataDir
		},
	})

	run(t, testCase{
		desc: "-datacenter empty",
		args: []string{
			`-auto-reload-config`,
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.AutoReloadConfig = true
			rt.DataDir = dataDir
		},
	})

	// ------------------------------------------------------------
	// ports and addresses
	//

	run(t, testCase{
		desc: "bind addr any v4",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr":"0.0.0.0" }`},
		hcl:  []string{`bind_addr = "0.0.0.0"`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("10.0.0.1")
			rt.AdvertiseAddrWAN = ipAddr("10.0.0.1")
			rt.BindAddr = ipAddr("0.0.0.0")
			rt.RPCAdvertiseAddr = tcpAddr("10.0.0.1:8300")
			rt.RPCBindAddr = tcpAddr("0.0.0.0:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("10.0.0.1:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("10.0.0.1:8302")
			rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:8301")
			rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "10.0.0.1",
				"lan_ipv4": "10.0.0.1",
				"wan":      "10.0.0.1",
				"wan_ipv4": "10.0.0.1",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "bind addr any v6",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr":"::" }`},
		hcl:  []string{`bind_addr = "::"`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("dead:beef::1")
			rt.AdvertiseAddrWAN = ipAddr("dead:beef::1")
			rt.BindAddr = ipAddr("::")
			rt.RPCAdvertiseAddr = tcpAddr("[dead:beef::1]:8300")
			rt.RPCBindAddr = tcpAddr("[::]:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("[dead:beef::1]:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("[dead:beef::1]:8302")
			rt.SerfBindAddrLAN = tcpAddr("[::]:8301")
			rt.SerfBindAddrWAN = tcpAddr("[::]:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "dead:beef::1",
				"lan_ipv6": "dead:beef::1",
				"wan":      "dead:beef::1",
				"wan_ipv6": "dead:beef::1",
			}
			rt.DataDir = dataDir
		},
		opts: LoadOpts{
			getPublicIPv6: func() ([]*net.IPAddr, error) {
				return []*net.IPAddr{ipAddr("dead:beef::1")}, nil
			},
		},
	})
	run(t, testCase{
		desc: "bind addr any and advertise set should not detect",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr":"0.0.0.0", "advertise_addr": "1.2.3.4" }`},
		hcl:  []string{`bind_addr = "0.0.0.0" advertise_addr = "1.2.3.4"`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("1.2.3.4")
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.BindAddr = ipAddr("0.0.0.0")
			rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300")
			rt.RPCBindAddr = tcpAddr("0.0.0.0:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:8301")
			rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.2.3.4",
				"lan_ipv4": "1.2.3.4",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
		opts: LoadOpts{
			getPrivateIPv4: func() ([]*net.IPAddr, error) {
				return nil, fmt.Errorf("should not detect advertise_addr")
			},
		},
	})
	run(t, testCase{
		desc:        "locality invalid",
		args:        []string{`-data-dir=` + dataDir},
		json:        []string{`{"locality": {"zone": "us-west-1a"}}`},
		hcl:         []string{`locality { zone = "us-west-1a" }`},
		expectedErr: "locality is invalid: zone cannot be set without region",
	})
	run(t, testCase{
		desc: "client addr and ports == 0",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr":"0.0.0.0",
					"ports":{}
				}`},
		hcl: []string{`
					client_addr = "0.0.0.0"
					ports {}
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")}
			rt.DNSAddrs = []net.Addr{tcpAddr("0.0.0.0:8600"), udpAddr("0.0.0.0:8600")}
			rt.HTTPAddrs = []net.Addr{tcpAddr("0.0.0.0:8500")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client addr and ports < 0",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr":"0.0.0.0",
					"ports": { "dns":-1, "http":-2, "https":-3, "grpc":-4 }
				}`},
		hcl: []string{`
					client_addr = "0.0.0.0"
					ports { dns = -1 http = -2 https = -3 grpc = -4 }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")}
			rt.DNSPort = -1
			rt.DNSAddrs = nil
			rt.HTTPPort = -1
			rt.HTTPAddrs = nil
			// HTTPS and gRPC default to disabled so shouldn't be different from
			// default rt.
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client addr and ports > 0",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr":"0.0.0.0",
					"ports":{ "dns": 1, "http": 2, "https": 3, "grpc": 4 }
				}`},
		hcl: []string{`
					client_addr = "0.0.0.0"
					ports { dns = 1 http = 2 https = 3 grpc = 4 }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")}
			rt.DNSPort = 1
			rt.DNSAddrs = []net.Addr{tcpAddr("0.0.0.0:1"), udpAddr("0.0.0.0:1")}
			rt.HTTPPort = 2
			rt.HTTPAddrs = []net.Addr{tcpAddr("0.0.0.0:2")}
			rt.HTTPSPort = 3
			rt.HTTPSAddrs = []net.Addr{tcpAddr("0.0.0.0:3")}
			rt.GRPCPort = 4
			rt.GRPCAddrs = []net.Addr{tcpAddr("0.0.0.0:4")}
			rt.DataDir = dataDir
		},
	})

	run(t, testCase{
		desc: "client addr, addresses and ports == 0",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr":"0.0.0.0",
					"addresses": { "dns": "1.1.1.1", "http": "2.2.2.2", "https": "3.3.3.3", "grpc": "4.4.4.4" },
					"ports":{}
				}`},
		hcl: []string{`
					client_addr = "0.0.0.0"
					addresses = { dns = "1.1.1.1" http = "2.2.2.2" https = "3.3.3.3" grpc = "4.4.4.4" }
					ports {}
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")}
			rt.DNSAddrs = []net.Addr{tcpAddr("1.1.1.1:8600"), udpAddr("1.1.1.1:8600")}
			rt.HTTPAddrs = []net.Addr{tcpAddr("2.2.2.2:8500")}
			// HTTPS and gRPC default to disabled so shouldn't be different from
			// default rt.
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client addr, addresses and ports < 0",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr":"0.0.0.0",
					"addresses": { "dns": "1.1.1.1", "http": "2.2.2.2", "https": "3.3.3.3", "grpc": "4.4.4.4" },
					"ports": { "dns":-1, "http":-2, "https":-3, "grpc":-4 }
				}`},
		hcl: []string{`
					client_addr = "0.0.0.0"
					addresses = { dns = "1.1.1.1" http = "2.2.2.2" https = "3.3.3.3" grpc = "4.4.4.4" }
					ports { dns = -1 http = -2 https = -3 grpc = -4 }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")}
			rt.DNSPort = -1
			rt.DNSAddrs = nil
			rt.HTTPPort = -1
			rt.HTTPAddrs = nil
			// HTTPS and gRPC default to disabled so shouldn't be different from
			// default rt.
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client addr, addresses and ports",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr": "0.0.0.0",
					"addresses": { "dns": "1.1.1.1", "http": "2.2.2.2", "https": "3.3.3.3", "grpc": "4.4.4.4" },
					"ports":{ "dns":1, "http":2, "https":3, "grpc":4 }
				}`},
		hcl: []string{`
					client_addr = "0.0.0.0"
					addresses = { dns = "1.1.1.1" http = "2.2.2.2" https = "3.3.3.3" grpc = "4.4.4.4" }
					ports { dns = 1 http = 2 https = 3 grpc = 4 }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")}
			rt.DNSPort = 1
			rt.DNSAddrs = []net.Addr{tcpAddr("1.1.1.1:1"), udpAddr("1.1.1.1:1")}
			rt.HTTPPort = 2
			rt.HTTPAddrs = []net.Addr{tcpAddr("2.2.2.2:2")}
			rt.HTTPSPort = 3
			rt.HTTPSAddrs = []net.Addr{tcpAddr("3.3.3.3:3")}
			rt.GRPCPort = 4
			rt.GRPCAddrs = []net.Addr{tcpAddr("4.4.4.4:4")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client template and ports",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr": "{{ printf \"1.2.3.4 2001:db8::1\" }}",
					"ports":{ "dns":1, "http":2, "https":3, "grpc":4 }
				}`},
		hcl: []string{`
					client_addr = "{{ printf \"1.2.3.4 2001:db8::1\" }}"
					ports { dns = 1 http = 2 https = 3 grpc = 4 }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("1.2.3.4"), ipAddr("2001:db8::1")}
			rt.DNSPort = 1
			rt.DNSAddrs = []net.Addr{tcpAddr("1.2.3.4:1"), tcpAddr("[2001:db8::1]:1"), udpAddr("1.2.3.4:1"), udpAddr("[2001:db8::1]:1")}
			rt.HTTPPort = 2
			rt.HTTPAddrs = []net.Addr{tcpAddr("1.2.3.4:2"), tcpAddr("[2001:db8::1]:2")}
			rt.HTTPSPort = 3
			rt.HTTPSAddrs = []net.Addr{tcpAddr("1.2.3.4:3"), tcpAddr("[2001:db8::1]:3")}
			rt.GRPCPort = 4
			rt.GRPCAddrs = []net.Addr{tcpAddr("1.2.3.4:4"), tcpAddr("[2001:db8::1]:4")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client, address template and ports",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
					"client_addr": "{{ printf \"1.2.3.4 2001:db8::1\" }}",
					"addresses": {
						"dns": "{{ printf \"1.1.1.1 2001:db8::10 \" }}",
						"http": "{{ printf \"2.2.2.2 unix://http 2001:db8::20 \" }}",
						"https": "{{ printf \"3.3.3.3 unix://https 2001:db8::30 \" }}",
						"grpc": "{{ printf \"4.4.4.4 unix://grpc 2001:db8::40 \" }}"
					},
					"ports":{ "dns":1, "http":2, "https":3, "grpc":4 }
				}`},
		hcl: []string{`
					client_addr = "{{ printf \"1.2.3.4 2001:db8::1\" }}"
					addresses = {
						dns = "{{ printf \"1.1.1.1 2001:db8::10 \" }}"
						http = "{{ printf \"2.2.2.2 unix://http 2001:db8::20 \" }}"
						https = "{{ printf \"3.3.3.3 unix://https 2001:db8::30 \" }}"
						grpc = "{{ printf \"4.4.4.4 unix://grpc 2001:db8::40 \" }}"
					}
					ports { dns = 1 http = 2 https = 3 grpc = 4 }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.ClientAddrs = []*net.IPAddr{ipAddr("1.2.3.4"), ipAddr("2001:db8::1")}
			rt.DNSPort = 1
			rt.DNSAddrs = []net.Addr{tcpAddr("1.1.1.1:1"), tcpAddr("[2001:db8::10]:1"), udpAddr("1.1.1.1:1"), udpAddr("[2001:db8::10]:1")}
			rt.HTTPPort = 2
			rt.HTTPAddrs = []net.Addr{tcpAddr("2.2.2.2:2"), unixAddr("unix://http"), tcpAddr("[2001:db8::20]:2")}
			rt.HTTPSPort = 3
			rt.HTTPSAddrs = []net.Addr{tcpAddr("3.3.3.3:3"), unixAddr("unix://https"), tcpAddr("[2001:db8::30]:3")}
			rt.GRPCPort = 4
			rt.GRPCAddrs = []net.Addr{tcpAddr("4.4.4.4:4"), unixAddr("unix://grpc"), tcpAddr("[2001:db8::40]:4")}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "advertise address lan template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "advertise_addr": "{{ printf \"1.2.3.4\" }}" }`},
		hcl:  []string{`advertise_addr = "{{ printf \"1.2.3.4\" }}"`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("1.2.3.4")
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.2.3.4",
				"lan_ipv4": "1.2.3.4",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "advertise address wan template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "advertise_addr_wan": "{{ printf \"1.2.3.4\" }}" }`},
		hcl:  []string{`advertise_addr_wan = "{{ printf \"1.2.3.4\" }}"`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.TaggedAddresses = map[string]string{
				"lan":      "10.0.0.1",
				"lan_ipv4": "10.0.0.1",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "advertise address lan with ports",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ports": {
					"server": 1000,
					"serf_lan": 2000,
					"serf_wan": 3000
				},
				"advertise_addr": "1.2.3.4"
			}`},
		hcl: []string{`
				ports {
					server = 1000
					serf_lan = 2000
					serf_wan = 3000
				}
				advertise_addr = "1.2.3.4"
			`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("1.2.3.4")
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:1000")
			rt.RPCBindAddr = tcpAddr("0.0.0.0:1000")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:2000")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:3000")
			rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:2000")
			rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:3000")
			rt.SerfPortLAN = 2000
			rt.SerfPortWAN = 3000
			rt.ServerPort = 1000
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.2.3.4",
				"lan_ipv4": "1.2.3.4",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "advertise address wan with ports",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ports": {
					"server": 1000,
					"serf_lan": 2000,
					"serf_wan": 3000
				},
				"advertise_addr_wan": "1.2.3.4"
			}`},
		hcl: []string{`
				ports {
					server = 1000
					serf_lan = 2000
					serf_wan = 3000
				}
				advertise_addr_wan = "1.2.3.4"
			`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("10.0.0.1")
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.RPCAdvertiseAddr = tcpAddr("10.0.0.1:1000")
			rt.RPCBindAddr = tcpAddr("0.0.0.0:1000")
			rt.SerfAdvertiseAddrLAN = tcpAddr("10.0.0.1:2000")
			rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:3000")
			rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:2000")
			rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:3000")
			rt.SerfPortLAN = 2000
			rt.SerfPortWAN = 3000
			rt.ServerPort = 1000
			rt.TaggedAddresses = map[string]string{
				"lan":      "10.0.0.1",
				"lan_ipv4": "10.0.0.1",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "allow disabling serf wan port",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ports": {
					"serf_wan": -1
				},
				"advertise_addr_wan": "1.2.3.4"
			}`},
		hcl: []string{`
				ports {
					serf_wan = -1
				}
				advertise_addr_wan = "1.2.3.4"
			`},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrWAN = ipAddr("1.2.3.4")
			rt.SerfAdvertiseAddrWAN = nil
			rt.SerfBindAddrWAN = nil
			rt.TaggedAddresses = map[string]string{
				"lan":      "10.0.0.1",
				"lan_ipv4": "10.0.0.1",
				"wan":      "1.2.3.4",
				"wan_ipv4": "1.2.3.4",
			}
			rt.DataDir = dataDir
			rt.SerfPortWAN = -1
		},
	})
	run(t, testCase{
		desc: "serf bind address lan template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "serf_lan": "{{ printf \"1.2.3.4\" }}" }`},
		hcl:  []string{`serf_lan = "{{ printf \"1.2.3.4\" }}"`},
		expected: func(rt *RuntimeConfig) {
			rt.SerfBindAddrLAN = tcpAddr("1.2.3.4:8301")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "serf bind address wan template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "serf_wan": "{{ printf \"1.2.3.4\" }}" }`},
		hcl:  []string{`serf_wan = "{{ printf \"1.2.3.4\" }}"`},
		expected: func(rt *RuntimeConfig) {
			rt.SerfBindAddrWAN = tcpAddr("1.2.3.4:8302")
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "dns recursor templates with deduplication",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "recursors": [ "{{ printf \"5.6.7.8:9999\" }}", "{{ printf \"1.2.3.4\" }}", "{{ printf \"5.6.7.8:9999\" }}" ] }`},
		hcl:  []string{`recursors = [ "{{ printf \"5.6.7.8:9999\" }}", "{{ printf \"1.2.3.4\" }}", "{{ printf \"5.6.7.8:9999\" }}" ] `},
		expected: func(rt *RuntimeConfig) {
			rt.DNSRecursors = []string{"5.6.7.8:9999", "1.2.3.4"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "start_join address template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "start_join": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`},
		hcl:  []string{`start_join = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinLAN = []string{"1.2.3.4", "4.3.2.1"}
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{
			deprecationWarning("start_join", "retry_join"),
		},
	})
	run(t, testCase{
		desc: "start_join_wan address template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "start_join_wan": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`},
		hcl:  []string{`start_join_wan = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinWAN = []string{"1.2.3.4", "4.3.2.1"}
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{
			deprecationWarning("start_join_wan", "retry_join_wan"),
		},
	})
	run(t, testCase{
		desc: "retry_join address template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "retry_join": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`},
		hcl:  []string{`retry_join = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinLAN = []string{"1.2.3.4", "4.3.2.1"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "retry_join_wan address template",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "retry_join_wan": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`},
		hcl:  []string{`retry_join_wan = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`},
		expected: func(rt *RuntimeConfig) {
			rt.RetryJoinWAN = []string{"1.2.3.4", "4.3.2.1"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "min/max ports for dynamic exposed listeners",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ports": {
					"expose_min_port": 1234,
					"expose_max_port": 5678
				}
			}`},
		hcl: []string{`
				ports {
					expose_min_port = 1234
					expose_max_port = 5678
				}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.ExposeMinPort = 1234
			rt.ExposeMaxPort = 5678
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "defaults for dynamic exposed listeners",
		args: []string{`-data-dir=` + dataDir},
		expected: func(rt *RuntimeConfig) {
			rt.ExposeMinPort = 21500
			rt.ExposeMaxPort = 21755
			rt.DataDir = dataDir
		},
	})

	// ------------------------------------------------------------
	// precedence rules
	//

	run(t, testCase{
		desc: "precedence: merge order",
		args: []string{`-data-dir=` + dataDir},
		json: []string{
			`{
						"bootstrap": true,
						"bootstrap_expect": 1,
						"datacenter": "a",
						"start_join": ["a", "b"],
						"node_meta": {"a":"b"}
					}`,
			`{
						"bootstrap": false,
						"bootstrap_expect": 0,
						"datacenter":"b",
						"start_join": ["c", "d"],
						"node_meta": {"a":"c"}
					}`,
		},
		hcl: []string{
			`
					bootstrap = true
					bootstrap_expect = 1
					datacenter = "a"
					start_join = ["a", "b"]
					node_meta = { "a" = "b" }
					`,
			`
					bootstrap = false
					bootstrap_expect = 0
					datacenter = "b"
					start_join = ["c", "d"]
					node_meta = { "a" = "c" }
					`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Bootstrap = false
			rt.BootstrapExpect = 0
			rt.Datacenter = "b"
			rt.PrimaryDatacenter = "b"
			rt.RetryJoinLAN = []string{"a", "b", "c", "d"}
			rt.NodeMeta = map[string]string{"a": "c"}
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{
			// TODO: deduplicate warnings?
			deprecationWarning("start_join", "retry_join"),
			deprecationWarning("start_join", "retry_join"),
		},
	})
	run(t, testCase{
		desc: "precedence: flag before file",
		json: []string{
			`{
						"advertise_addr": "1.2.3.4",
						"advertise_addr_wan": "5.6.7.8",
						"bootstrap":true,
						"bootstrap_expect": 3,
						"datacenter":"a",
						"node_meta": {"a":"b"},
						"recursors":["1.2.3.5", "5.6.7.9"],
						"serf_lan": "a",
						"serf_wan": "a",
						"start_join":["a", "b"]
					}`,
		},
		hcl: []string{`
					advertise_addr = "1.2.3.4"
					advertise_addr_wan = "5.6.7.8"
					bootstrap = true
					bootstrap_expect = 3
					datacenter = "a"
					node_meta = { "a" = "b" }
					recursors = ["1.2.3.5", "5.6.7.9"]
					serf_lan = "a"
					serf_wan = "a"
					start_join = ["a", "b"]
					`,
		},
		args: []string{
			`-advertise=1.1.1.1`,
			`-advertise-wan=2.2.2.2`,
			`-bootstrap=false`,
			`-bootstrap-expect=0`,
			`-datacenter=b`,
			`-data-dir=` + dataDir,
			`-join`, `c`, `-join=d`,
			`-node-meta=a:c`,
			`-recursor`, `1.2.3.6`, `-recursor=5.6.7.10`,
			`-serf-lan-bind=3.3.3.3`,
			`-serf-wan-bind=4.4.4.4`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.AdvertiseAddrLAN = ipAddr("1.1.1.1")
			rt.AdvertiseAddrWAN = ipAddr("2.2.2.2")
			rt.RPCAdvertiseAddr = tcpAddr("1.1.1.1:8300")
			rt.SerfAdvertiseAddrLAN = tcpAddr("1.1.1.1:8301")
			rt.SerfAdvertiseAddrWAN = tcpAddr("2.2.2.2:8302")
			rt.Datacenter = "b"
			rt.PrimaryDatacenter = "b"
			rt.DNSRecursors = []string{"1.2.3.6", "5.6.7.10", "1.2.3.5", "5.6.7.9"}
			rt.NodeMeta = map[string]string{"a": "c"}
			rt.SerfBindAddrLAN = tcpAddr("3.3.3.3:8301")
			rt.SerfBindAddrWAN = tcpAddr("4.4.4.4:8302")
			rt.RetryJoinLAN = []string{"c", "d", "a", "b"}
			rt.TaggedAddresses = map[string]string{
				"lan":      "1.1.1.1",
				"lan_ipv4": "1.1.1.1",
				"wan":      "2.2.2.2",
				"wan_ipv4": "2.2.2.2",
			}
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{
			deprecatedFlagWarning("-join", "-retry-join"),
			deprecationWarning("start_join", "retry_join"),
		},
	})

	// ------------------------------------------------------------
	// transformations
	//

	run(t, testCase{
		desc: "raft performance scaling",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "performance": { "raft_multiplier": 9} }`},
		hcl:  []string{`performance = { raft_multiplier=9 }`},
		expected: func(rt *RuntimeConfig) {
			rt.ConsulRaftElectionTimeout = 9 * 1000 * time.Millisecond
			rt.ConsulRaftHeartbeatTimeout = 9 * 1000 * time.Millisecond
			rt.ConsulRaftLeaderLeaseTimeout = 9 * 500 * time.Millisecond
			rt.DataDir = dataDir
		},
	})

	run(t, testCase{
		desc: "Serf Allowed CIDRS LAN, multiple values from flags",
		args: []string{`-data-dir=` + dataDir, `-serf-lan-allowed-cidrs=127.0.0.0/4`, `-serf-lan-allowed-cidrs=192.168.0.0/24`},
		json: []string{},
		hcl:  []string{},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.SerfAllowedCIDRsLAN = []net.IPNet{*(parseCIDR(t, "127.0.0.0/4")), *(parseCIDR(t, "192.168.0.0/24"))}
		},
	})
	run(t, testCase{
		desc: "Serf Allowed CIDRS LAN/WAN, multiple values from HCL/JSON",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{"serf_lan_allowed_cidrs": ["127.0.0.0/4", "192.168.0.0/24"]}`,
			`{"serf_wan_allowed_cidrs": ["10.228.85.46/25"]}`},
		hcl: []string{`serf_lan_allowed_cidrs=["127.0.0.0/4", "192.168.0.0/24"]`,
			`serf_wan_allowed_cidrs=["10.228.85.46/25"]`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.SerfAllowedCIDRsLAN = []net.IPNet{*(parseCIDR(t, "127.0.0.0/4")), *(parseCIDR(t, "192.168.0.0/24"))}
			rt.SerfAllowedCIDRsWAN = []net.IPNet{*(parseCIDR(t, "10.228.85.46/25"))}
		},
	})
	run(t, testCase{
		desc: "Serf Allowed CIDRS WAN, multiple values from flags",
		args: []string{`-data-dir=` + dataDir, `-serf-wan-allowed-cidrs=192.168.4.0/24`, `-serf-wan-allowed-cidrs=192.168.3.0/24`},
		json: []string{},
		hcl:  []string{},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.SerfAllowedCIDRsWAN = []net.IPNet{*(parseCIDR(t, "192.168.4.0/24")), *(parseCIDR(t, "192.168.3.0/24"))}
		},
	})

	// ------------------------------------------------------------
	// validations
	//

	run(t, testCase{
		desc:        "invalid input",
		args:        []string{`-data-dir=` + dataDir},
		json:        []string{`this is not JSON`},
		hcl:         []string{`*** 0123 this is not HCL`},
		expectedErr: "failed to parse",
	})
	run(t, testCase{
		desc: "datacenter is lower-cased",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "datacenter": "A" }`},
		hcl:  []string{`datacenter = "A"`},
		expected: func(rt *RuntimeConfig) {
			rt.Datacenter = "a"
			rt.PrimaryDatacenter = "a"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "acl_datacenter is lower-cased",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "acl_datacenter": "A" }`},
		hcl:  []string{`acl_datacenter = "A"`},
		expected: func(rt *RuntimeConfig) {
			rt.ACLsEnabled = true
			rt.DataDir = dataDir
			rt.PrimaryDatacenter = "a"
		},
		expectedWarnings: []string{`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`},
	})
	run(t, testCase{
		desc:             "acl_replication_token enables acl replication",
		args:             []string{`-data-dir=` + dataDir},
		json:             []string{`{ "acl_replication_token": "a" }`},
		hcl:              []string{`acl_replication_token = "a"`},
		expectedWarnings: []string{deprecationWarning("acl_replication_token", "acl.tokens.replication")},
		expected: func(rt *RuntimeConfig) {
			rt.ACLTokens.ACLReplicationToken = "a"
			rt.ACLTokenReplication = true
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "acl.tokens.replace does not enable acl replication",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "acl": { "tokens": { "replication": "a" }}}`},
		hcl:  []string{`acl { tokens { replication = "a"}}`},
		expected: func(rt *RuntimeConfig) {
			rt.ACLTokens.ACLReplicationToken = "a"
			rt.ACLTokenReplication = false
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "acl_enforce_version_8 is deprecated",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "acl_enforce_version_8": true }`},
		hcl:  []string{`acl_enforce_version_8 = true`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{`config key "acl_enforce_version_8" is deprecated and should be removed`},
	})

	run(t, testCase{
		desc: "advertise address detect fails v4",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr": "0.0.0.0"}`},
		hcl:  []string{`bind_addr = "0.0.0.0"`},
		opts: LoadOpts{
			getPrivateIPv4: func() ([]*net.IPAddr, error) {
				return nil, errors.New("some error")
			},
		},
		expectedErr: "Error detecting private IPv4 address: some error",
	})
	run(t, testCase{
		desc: "advertise address detect none v4",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr": "0.0.0.0"}`},
		hcl:  []string{`bind_addr = "0.0.0.0"`},
		opts: LoadOpts{
			getPrivateIPv4: func() ([]*net.IPAddr, error) {
				return nil, nil
			},
		},
		expectedErr: "No private IPv4 address found",
	})
	run(t, testCase{
		desc: "advertise address detect multiple v4",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr": "0.0.0.0"}`},
		hcl:  []string{`bind_addr = "0.0.0.0"`},
		opts: LoadOpts{
			getPrivateIPv4: func() ([]*net.IPAddr, error) {
				return []*net.IPAddr{ipAddr("1.1.1.1"), ipAddr("2.2.2.2")}, nil
			},
		},
		expectedErr: "Multiple private IPv4 addresses found. Please configure one",
	})
	run(t, testCase{
		desc: "advertise address detect fails v6",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr": "::"}`},
		hcl:  []string{`bind_addr = "::"`},
		opts: LoadOpts{
			getPublicIPv6: func() ([]*net.IPAddr, error) {
				return nil, errors.New("some error")
			},
		},
		expectedErr: "Error detecting public IPv6 address: some error",
	})
	run(t, testCase{
		desc: "advertise address detect none v6",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr": "::"}`},
		hcl:  []string{`bind_addr = "::"`},
		opts: LoadOpts{
			getPublicIPv6: func() ([]*net.IPAddr, error) {
				return nil, nil
			},
		},
		expectedErr: "No public IPv6 address found",
	})
	run(t, testCase{
		desc: "advertise address detect multiple v6",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "bind_addr": "::"}`},
		hcl:  []string{`bind_addr = "::"`},
		opts: LoadOpts{
			getPublicIPv6: func() ([]*net.IPAddr, error) {
				return []*net.IPAddr{ipAddr("dead:beef::1"), ipAddr("dead:beef::2")}, nil
			},
		},
		expectedErr: "Multiple public IPv6 addresses found. Please configure one",
	})
	run(t, testCase{
		desc: "ae_interval is overridden by NonUserSource",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "ae_interval": "-1s" }`},
		hcl:  []string{`ae_interval = "-1s"`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.AEInterval = time.Minute
		},
	})
	run(t, testCase{
		desc: "primary_datacenter invalid",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "primary_datacenter": "%" }`},
		hcl:         []string{`primary_datacenter = "%"`},
		expectedErr: `primary_datacenter can only contain lowercase alphanumeric, - or _ characters.`,
	})
	run(t, testCase{
		desc: "acl_datacenter deprecated",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:             []string{`{ "acl_datacenter": "ab" }`},
		hcl:              []string{`acl_datacenter = "ab"`},
		expectedWarnings: []string{`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`},
		expected: func(rt *RuntimeConfig) {
			rt.ACLsEnabled = true
			rt.PrimaryDatacenter = "ab"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "autopilot.max_trailing_logs invalid",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "autopilot": { "max_trailing_logs": -1 } }`},
		hcl:         []string{`autopilot = { max_trailing_logs = -1 }`},
		expectedErr: "autopilot.max_trailing_logs cannot be -1. Must be greater than or equal to zero",
	})
	run(t, testCase{
		desc:        "bind_addr cannot be empty",
		args:        []string{`-data-dir=` + dataDir},
		json:        []string{`{ "bind_addr": "" }`},
		hcl:         []string{`bind_addr = ""`},
		expectedErr: "bind_addr cannot be empty",
	})
	run(t, testCase{
		desc:        "bind_addr does not allow multiple addresses",
		args:        []string{`-data-dir=` + dataDir},
		json:        []string{`{ "bind_addr": "1.1.1.1 2.2.2.2" }`},
		hcl:         []string{`bind_addr = "1.1.1.1 2.2.2.2"`},
		expectedErr: "bind_addr cannot contain multiple addresses",
	})
	run(t, testCase{
		desc:        "bind_addr cannot be a unix socket",
		args:        []string{`-data-dir=` + dataDir},
		json:        []string{`{ "bind_addr": "unix:///foo" }`},
		hcl:         []string{`bind_addr = "unix:///foo"`},
		expectedErr: "bind_addr cannot be a unix socket",
	})
	run(t, testCase{
		desc: "bootstrap without server",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "bootstrap": true }`},
		hcl:         []string{`bootstrap = true`},
		expectedErr: "'bootstrap = true' requires 'server = true'",
	})
	run(t, testCase{
		desc: "bootstrap-expect without server",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "bootstrap_expect": 3 }`},
		hcl:         []string{`bootstrap_expect = 3`},
		expectedErr: "'bootstrap_expect > 0' requires 'server = true'",
	})
	run(t, testCase{
		desc: "bootstrap-expect invalid",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "bootstrap_expect": -1 }`},
		hcl:         []string{`bootstrap_expect = -1`},
		expectedErr: "bootstrap_expect cannot be -1. Must be greater than or equal to zero",
	})
	run(t, testCase{
		desc: "bootstrap-expect and dev mode",
		args: []string{
			`-dev`,
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "bootstrap_expect": 3, "server": true }`},
		hcl:         []string{`bootstrap_expect = 3 server = true`},
		expectedErr: "'bootstrap_expect > 0' not allowed in dev mode",
	})
	run(t, testCase{
		desc: "bootstrap-expect and bootstrap",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "bootstrap": true, "bootstrap_expect": 3, "server": true }`},
		hcl:         []string{`bootstrap = true bootstrap_expect = 3 server = true`},
		expectedErr: "'bootstrap_expect > 0' and 'bootstrap = true' are mutually exclusive",
	})
	run(t, testCase{
		desc: "bootstrap-expect=1 equals bootstrap",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "bootstrap_expect": 1, "server": true }`},
		hcl:  []string{`bootstrap_expect = 1 server = true`},
		expected: func(rt *RuntimeConfig) {
			rt.Bootstrap = true
			rt.BootstrapExpect = 0
			rt.LeaveOnTerm = false
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.SkipLeaveOnInt = true
			rt.DataDir = dataDir
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
		expectedWarnings: []string{"BootstrapExpect is set to 1; this is the same as Bootstrap mode.", "bootstrap = true: do not enable unless necessary"},
	})
	run(t, testCase{
		desc: "bootstrap-expect=2 warning",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "bootstrap_expect": 2, "server": true }`},
		hcl:  []string{`bootstrap_expect = 2 server = true`},
		expected: func(rt *RuntimeConfig) {
			rt.BootstrapExpect = 2
			rt.LeaveOnTerm = false
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.SkipLeaveOnInt = true
			rt.DataDir = dataDir
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
		expectedWarnings: []string{
			`bootstrap_expect = 2: A cluster with 2 servers will provide no failure tolerance. See https://developer.hashicorp.com/docs/internals/consensus.html#deployment-table`,
			`bootstrap_expect > 0: expecting 2 servers`,
		},
	})
	run(t, testCase{
		desc: "bootstrap-expect > 2 but even warning",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "bootstrap_expect": 4, "server": true }`},
		hcl:  []string{`bootstrap_expect = 4 server = true`},
		expected: func(rt *RuntimeConfig) {
			rt.BootstrapExpect = 4
			rt.LeaveOnTerm = false
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.SkipLeaveOnInt = true
			rt.DataDir = dataDir
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
		expectedWarnings: []string{
			`bootstrap_expect is even number: A cluster with an even number of servers does not achieve optimum fault tolerance. See https://developer.hashicorp.com/docs/internals/consensus.html#deployment-table`,
			`bootstrap_expect > 0: expecting 4 servers`,
		},
	})
	run(t, testCase{
		desc: "client mode sets LeaveOnTerm and SkipLeaveOnInt correctly",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "server": false }`},
		hcl:  []string{` server = false`},
		expected: func(rt *RuntimeConfig) {
			rt.LeaveOnTerm = true
			rt.ServerMode = false
			rt.TLS.ServerMode = false
			rt.SkipLeaveOnInt = false
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "client does not allow socket",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "client_addr": "unix:///foo" }`},
		hcl:         []string{`client_addr = "unix:///foo"`},
		expectedErr: "client_addr cannot be a unix socket",
	})
	run(t, testCase{
		desc:        "datacenter invalid",
		args:        []string{`-data-dir=` + dataDir},
		json:        []string{`{ "datacenter": "%" }`},
		hcl:         []string{`datacenter = "%"`},
		expectedErr: `datacenter can only contain lowercase alphanumeric, - or _ characters.`,
	})
	run(t, testCase{
		desc: "dns does not allow socket",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "addresses": {"dns": "unix:///foo" } }`},
		hcl:         []string{`addresses = { dns = "unix:///foo" }`},
		expectedErr: "DNS address cannot be a unix socket",
	})
	run(t, testCase{
		desc: "ui enabled and dir specified",
		args: []string{
			`-datacenter=a`,
			`-data-dir=` + dataDir,
		},
		json: []string{`{ "ui_config": { "enabled": true, "dir": "a" } }`},
		hcl:  []string{`ui_config { enabled = true dir = "a"}`},
		expectedErr: "Both the ui_config.enabled and ui_config.dir (or -ui and -ui-dir) were specified, please provide only one.\n" +
			"If trying to use your own web UI resources, use ui_config.dir or the -ui-dir flag.\n" +
			"The web UI is included in the binary so use ui_config.enabled or the -ui flag to enable it",
	})

	// test ANY address failures
	// to avoid combinatory explosion for tests use 0.0.0.0, :: or [::] but not all of them
	run(t, testCase{
		desc: "advertise_addr any",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "advertise_addr": "0.0.0.0" }`},
		hcl:         []string{`advertise_addr = "0.0.0.0"`},
		expectedErr: "Advertise address cannot be 0.0.0.0, :: or [::]",
	})
	run(t, testCase{
		desc: "advertise_addr_wan any",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "advertise_addr_wan": "::" }`},
		hcl:         []string{`advertise_addr_wan = "::"`},
		expectedErr: "Advertise WAN address cannot be 0.0.0.0, :: or [::]",
	})
	run(t, testCase{
		desc: "recursors any",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "recursors": ["::"] }`},
		hcl:         []string{`recursors = ["::"]`},
		expectedErr: "DNS recursor address cannot be 0.0.0.0, :: or [::]",
	})
	run(t, testCase{
		desc: "dns_config.udp_answer_limit invalid",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "dns_config": { "udp_answer_limit": -1 } }`},
		hcl:         []string{`dns_config = { udp_answer_limit = -1 }`},
		expectedErr: "dns_config.udp_answer_limit cannot be -1. Must be greater than or equal to zero",
	})
	run(t, testCase{
		desc: "dns_config.a_record_limit invalid",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "dns_config": { "a_record_limit": -1 } }`},
		hcl:         []string{`dns_config = { a_record_limit = -1 }`},
		expectedErr: "dns_config.a_record_limit cannot be -1. Must be greater than or equal to zero",
	})
	run(t, testCase{
		desc: "performance.raft_multiplier < 0",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "performance": { "raft_multiplier": -1 } }`},
		hcl:         []string{`performance = { raft_multiplier = -1 }`},
		expectedErr: `performance.raft_multiplier cannot be -1. Must be between 1 and 10`,
	})
	run(t, testCase{
		desc: "performance.raft_multiplier == 0",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "performance": { "raft_multiplier": 0 } }`},
		hcl:         []string{`performance = { raft_multiplier = 0 }`},
		expectedErr: `performance.raft_multiplier cannot be 0. Must be between 1 and 10`,
	})
	run(t, testCase{
		desc: "performance.raft_multiplier > 10",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "performance": { "raft_multiplier": 20 } }`},
		hcl:         []string{`performance = { raft_multiplier = 20 }`},
		expectedErr: `performance.raft_multiplier cannot be 20. Must be between 1 and 10`,
	})
	run(t, testCase{
		desc: "disable XDS Load balancing",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{ "performance": { "enable_xds_load_balancing": false} }`},
		hcl:  []string{`performance = { enable_xds_load_balancing=false }`},
		expected: func(rt *RuntimeConfig) {
			rt.EnableXDSLoadBalancing = false
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "node_name invalid",
		args: []string{
			`-data-dir=` + dataDir,
			`-node=`,
		},
		opts: LoadOpts{
			hostname: func() (string, error) { return "", nil },
		},
		expectedErr: "node_name cannot be empty",
	})
	run(t, testCase{
		desc: "node_meta key too long",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "dns_config": { "udp_answer_limit": 1 } }`,
			`{ "node_meta": { "` + randomString(130) + `": "a" } }`,
		},
		hcl: []string{
			`dns_config = { udp_answer_limit = 1 }`,
			`node_meta = { "` + randomString(130) + `" = "a" }`,
		},
		expectedErr: "Key is too long (limit: 128 characters)",
	})
	run(t, testCase{
		desc: "node_meta value too long",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "dns_config": { "udp_answer_limit": 1 } }`,
			`{ "node_meta": { "a": "` + randomString(520) + `" } }`,
		},
		hcl: []string{
			`dns_config = { udp_answer_limit = 1 }`,
			`node_meta = { "a" = "` + randomString(520) + `" }`,
		},
		expectedErr: "Value is too long (limit: 512 characters)",
	})
	run(t, testCase{
		desc: "node_meta too many keys",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "dns_config": { "udp_answer_limit": 1 } }`,
			`{ "node_meta": {` + metaPairs(70, "json") + `} }`,
		},
		hcl: []string{
			`dns_config = { udp_answer_limit = 1 }`,
			`node_meta = {` + metaPairs(70, "hcl") + ` }`,
		},
		expectedErr: "Node metadata cannot contain more than 64 key/value pairs",
	})
	run(t, testCase{
		desc: "unique listeners dns vs http",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"client_addr": "1.2.3.4",
					"ports": { "dns": 1000, "http": 1000 }
				}`},
		hcl: []string{`
					client_addr = "1.2.3.4"
					ports = { dns = 1000 http = 1000 }
				`},
		expectedErr: "HTTP address 1.2.3.4:1000 already configured for DNS",
	})
	run(t, testCase{
		desc: "unique listeners dns vs https",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"client_addr": "1.2.3.4",
					"ports": { "dns": 1000, "https": 1000 }
				}`},
		hcl: []string{`
					client_addr = "1.2.3.4"
					ports = { dns = 1000 https = 1000 }
				`},
		expectedErr: "HTTPS address 1.2.3.4:1000 already configured for DNS",
	})
	run(t, testCase{
		desc: "unique listeners http vs https",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"client_addr": "1.2.3.4",
					"ports": { "http": 1000, "https": 1000 }
				}`},
		hcl: []string{`
					client_addr = "1.2.3.4"
					ports = { http = 1000 https = 1000 }
				`},
		expectedErr: "HTTPS address 1.2.3.4:1000 already configured for HTTP",
	})
	run(t, testCase{
		desc: "unique advertise addresses HTTP vs RPC",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"addresses": { "http": "10.0.0.1" },
					"ports": { "http": 1000, "server": 1000 }
				}`},
		hcl: []string{`
					addresses = { http = "10.0.0.1" }
					ports = { http = 1000 server = 1000 }
				`},
		expectedErr: "RPC Advertise address 10.0.0.1:1000 already configured for HTTP",
	})
	run(t, testCase{
		desc: "unique advertise addresses RPC vs Serf LAN",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"ports": { "server": 1000, "serf_lan": 1000 }
				}`},
		hcl: []string{`
					ports = { server = 1000 serf_lan = 1000 }
				`},
		expectedErr: "Serf Advertise LAN address 10.0.0.1:1000 already configured for RPC Advertise",
	})
	run(t, testCase{
		desc: "unique advertise addresses RPC vs Serf WAN",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"ports": { "server": 1000, "serf_wan": 1000 }
				}`},
		hcl: []string{`
					ports = { server = 1000 serf_wan = 1000 }
				`},
		expectedErr: "Serf Advertise WAN address 10.0.0.1:1000 already configured for RPC Advertise",
	})
	run(t, testCase{
		desc: "http use_cache defaults to true",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"http_config": {}
			}`},
		hcl: []string{`
				http_config = {}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.HTTPUseCache = true
		},
	})
	run(t, testCase{
		desc: "http use_cache is enabled when true",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"http_config": { "use_cache": true }
			}`},
		hcl: []string{`
				http_config = { use_cache = true }
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.HTTPUseCache = true
		},
	})
	run(t, testCase{
		desc: "http use_cache is disabled when false",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"http_config": { "use_cache": false }
			}`},
		hcl: []string{`
				http_config = { use_cache = false }
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.HTTPUseCache = false
		},
	})
	run(t, testCase{
		desc: "cloud resource id from env",
		args: []string{
			`-server`,
			`-data-dir=` + dataDir,
		},
		setup: func() {
			os.Setenv("HCP_RESOURCE_ID", "env-id")
		},
		cleanup: func() {
			os.Unsetenv("HCP_RESOURCE_ID")
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Cloud = hcpconfig.CloudConfig{
				ResourceID: "env-id",
				NodeName:   "thehostname",
				NodeID:     "",
			}

			// server things
			rt.ServerMode = true
			rt.Telemetry.EnableHostMetrics = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "cloud resource id from file",
		args: []string{
			`-server`,
			`-data-dir=` + dataDir,
		},
		setup: func() {
			os.Setenv("HCP_RESOURCE_ID", "env-id")
		},
		cleanup: func() {
			os.Unsetenv("HCP_RESOURCE_ID")
		},
		json: []string{`{
			  "cloud": {
              	"resource_id": "file-id"
              }
			}`},
		hcl: []string{`
			  cloud = {
	            resource_id = "file-id"
			  }
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Cloud = hcpconfig.CloudConfig{
				ResourceID: "env-id",
				NodeName:   "thehostname",
			}

			// server things
			rt.ServerMode = true
			rt.Telemetry.EnableHostMetrics = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "sidecar_service can't have ID",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				  "service": {
						"name": "web",
						"port": 1234,
						"connect": {
							"sidecar_service": {
								"ID": "random-sidecar-id"
							}
						}
					}
				}`},
		hcl: []string{`
				service {
					name = "web"
					port = 1234
					connect {
						sidecar_service {
							ID = "random-sidecar-id"
						}
					}
				}
			`},
		expectedErr: "sidecar_service can't specify an ID",
	})
	run(t, testCase{
		desc: "sidecar_service can't have nested sidecar",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				  "service": {
						"name": "web",
						"port": 1234,
						"connect": {
							"sidecar_service": {
								"connect": {
									"sidecar_service": {}
								}
							}
						}
					}
				}`},
		hcl: []string{`
				service {
					name = "web"
					port = 1234
					connect {
						sidecar_service {
							connect {
								sidecar_service {
								}
							}
						}
					}
				}
			`},
		expectedErr: "sidecar_service can't have a nested sidecar_service",
	})
	run(t, testCase{
		desc: "telemetry.prefix_filter cannot be empty",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"telemetry": { "prefix_filter": [""] }
				}`},
		hcl: []string{`
					telemetry = { prefix_filter = [""] }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
		},
		expectedWarnings: []string{"Cannot have empty filter rule in prefix_filter"},
	})
	run(t, testCase{
		desc: "telemetry.prefix_filter must start with + or -",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
					"telemetry": { "prefix_filter": ["+foo", "-bar", "nix"] }
				}`},
		hcl: []string{`
					telemetry = { prefix_filter = ["+foo", "-bar", "nix"] }
				`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Telemetry.AllowedPrefixes = []string{"foo"}
			rt.Telemetry.BlockedPrefixes = []string{"bar", "consul.rpc.server.call"}
		},
		expectedWarnings: []string{`Filter rule must begin with either '+' or '-': "nix"`},
	})
	run(t, testCase{
		desc: "encrypt has invalid key",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json:        []string{`{ "encrypt": "this is not a valid key" }`},
		hcl:         []string{` encrypt = "this is not a valid key" `},
		expectedErr: "encrypt has invalid key: illegal base64 data at input byte 4",
	})
	run(t, testCase{
		desc: "multiple check files",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "args": ["/bin/true"], "interval": "1s" } }`,
			`{ "check": { "name": "b", "args": ["/bin/false"], "interval": "1s" } }`,
		},
		hcl: []string{
			`check = { name = "a" args = ["/bin/true"] interval = "1s"}`,
			`check = { name = "b" args = ["/bin/false"] interval = "1s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", ScriptArgs: []string{"/bin/true"}, OutputMaxSize: checks.DefaultBufSize, Interval: time.Second},
				{Name: "b", ScriptArgs: []string{"/bin/false"}, OutputMaxSize: checks.DefaultBufSize, Interval: time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "grpc check",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "grpc": "localhost:12345/foo", "grpc_use_tls": true, "interval": "1s" } }`,
		},
		hcl: []string{
			`check = { name = "a" grpc = "localhost:12345/foo", grpc_use_tls = true interval = "1s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", GRPC: "localhost:12345/foo", GRPCUseTLS: true, OutputMaxSize: checks.DefaultBufSize, Interval: time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "tcp check with tcp_use_tls set",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "tcp": "localhost:55555", "tcp_use_tls": true, "interval": "5s" } }`,
		},
		hcl: []string{
			`check = { name = "a" tcp = "localhost:55555" tcp_use_tls = true interval = "5s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", TCP: "localhost:55555", TCPUseTLS: true, OutputMaxSize: checks.DefaultBufSize, Interval: 5 * time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "tcp check with tcp_use_tls set to false",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "tcp": "localhost:55555", "tcp_use_tls": false, "interval": "5s" } }`,
		},
		hcl: []string{
			`check = { name = "a" tcp = "localhost:55555" tcp_use_tls = false interval = "5s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", TCP: "localhost:55555", TCPUseTLS: false, OutputMaxSize: checks.DefaultBufSize, Interval: 5 * time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "tcp check with tcp_use_tls not set",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "tcp": "localhost:55555", "interval": "5s" } }`,
		},
		hcl: []string{
			`check = { name = "a" tcp = "localhost:55555" interval = "5s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", TCP: "localhost:55555", TCPUseTLS: false, OutputMaxSize: checks.DefaultBufSize, Interval: 5 * time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "h2ping check without h2ping_use_tls set",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "h2ping": "localhost:55555", "interval": "5s" } }`,
		},
		hcl: []string{
			`check = { name = "a" h2ping = "localhost:55555" interval = "5s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", H2PING: "localhost:55555", H2PingUseTLS: true, OutputMaxSize: checks.DefaultBufSize, Interval: 5 * time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "h2ping check with h2ping_use_tls set to false",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "h2ping": "localhost:55555", "h2ping_use_tls": false, "interval": "5s" } }`,
		},
		hcl: []string{
			`check = { name = "a" h2ping = "localhost:55555" h2ping_use_tls = false interval = "5s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", H2PING: "localhost:55555", H2PingUseTLS: false, OutputMaxSize: checks.DefaultBufSize, Interval: 5 * time.Second},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "alias check with no node",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "alias_service": "foo" } }`,
		},
		hcl: []string{
			`check = { name = "a", alias_service = "foo" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a", AliasService: "foo", OutputMaxSize: checks.DefaultBufSize},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "os_service check no interval",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "os_service": "foo" } }`,
		},
		hcl: []string{
			`check = { name = "a", os_service = "foo" }`,
		},
		expectedErr: `Interval must be > 0 for Script, HTTP, H2PING, TCP, UDP or OSService checks`,
	})
	run(t, testCase{
		desc: "os_service check",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "check": { "name": "a", "os_service": "foo", "interval": "30s" } }`,
		},
		hcl: []string{
			`check = { name = "a", os_service = "foo", interval = "30s" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Checks = []*structs.CheckDefinition{
				{Name: "a",
					OSService:     "foo",
					Interval:      30 * time.Second,
					OutputMaxSize: checks.DefaultBufSize,
				},
			}
			rt.DataDir = dataDir
		}})
	run(t, testCase{
		desc: "multiple service files",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "service": { "name": "a", "port": 80 } }`,
			`{ "service": { "name": "b", "port": 90, "meta": {"my": "value"}, "weights": {"passing": 13} } }`,
		},
		hcl: []string{
			`service = { name = "a" port = 80 }`,
			`service = { name = "b" port = 90 meta={my="value"}, weights={passing=13}}`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Services = []*structs.ServiceDefinition{
				{
					Name: "a",
					Port: 80,
					Weights: &structs.Weights{
						Passing: 1,
						Warning: 1,
					},
				},
				{
					Name: "b",
					Port: 90,
					Meta: map[string]string{"my": "value"},
					Weights: &structs.Weights{
						Passing: 13,
						Warning: 1,
					},
				},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "service with wrong meta: too long key",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "service": { "name": "a", "port": 80, "meta": { "` + randomString(520) + `": "metaValue" } } }`,
		},
		hcl: []string{
			`service = { name = "a" port = 80, meta={` + randomString(520) + `="metaValue"} }`,
		},
		expectedErr: `Key is too long`,
	})
	run(t, testCase{
		desc: "service with wrong meta: too long value",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "service": { "name": "a", "port": 80, "meta": { "a": "` + randomString(520) + `" } } }`,
		},
		hcl: []string{
			`service = { name = "a" port = 80, meta={a="` + randomString(520) + `"} }`,
		},
		expectedErr: `Value is too long`,
	})
	run(t, testCase{
		desc: "service with wrong meta: too many meta",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "service": { "name": "a", "port": 80, "meta": { ` + metaPairs(70, "json") + `} } }`,
		},
		hcl: []string{
			`service = { name = "a" port = 80 meta={` + metaPairs(70, "hcl") + `} }`,
		},
		expectedErr: `invalid meta for service a: Node metadata cannot contain more than 64 key`,
	})
	run(t, testCase{
		desc: "verify_outgoing in the grpc stanza",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
			tls {
				grpc {
					verify_outgoing = true
				}
			}
		`},
		json: []string{`
			{
				"tls": {
					"grpc": {
						"verify_outgoing": true
					}
				}
			}
		`},
		expectedErr: "verify_outgoing is not valid in the tls.grpc stanza",
	})
	run(t, testCase{
		desc: "verify_server_hostname in the defaults stanza",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
			tls {
				defaults {
					verify_server_hostname = true
				}
			}
		`},
		json: []string{`
			{
				"tls": {
					"defaults": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.InternalRPC.VerifyServerHostname = true
			rt.TLS.InternalRPC.VerifyOutgoing = true
		},
	})
	run(t, testCase{
		desc: "verify_server_hostname in the defaults stanza and internal_rpc",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
			tls {
				defaults {
					verify_server_hostname = false
				},
				internal_rpc {
					verify_server_hostname = true
				}
			}
		`},
		json: []string{`
			{
				"tls": {
					"defaults": {
						"verify_server_hostname": false
					},
					"internal_rpc": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.InternalRPC.VerifyServerHostname = true
			rt.TLS.InternalRPC.VerifyOutgoing = true
		},
	})
	run(t, testCase{
		desc: "verify_server_hostname in the grpc stanza",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
			tls {
				grpc {
					verify_server_hostname = true
				}
			}
		`},
		json: []string{`
			{
				"tls": {
					"grpc": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		expectedErr: "verify_server_hostname is only valid in the tls.defaults and tls.internal_rpc stanza",
	})
	run(t, testCase{
		desc: "verify_server_hostname in the https stanza",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
			tls {
				https {
					verify_server_hostname = true
				}
			}
		`},
		json: []string{`
			{
				"tls": {
					"https": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		expectedErr: "verify_server_hostname is only valid in the tls.defaults and tls.internal_rpc stanza",
	})
	run(t, testCase{
		desc: "translated keys",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{
					"service": {
						"name": "a",
						"port": 80,
						"tagged_addresses": {
							"wan": {
								"address": "198.18.3.4",
								"port": 443
							}
						},
						"enable_tag_override": true,
						"check": {
							"id": "x",
							"name": "y",
							"DockerContainerID": "z",
							"DeregisterCriticalServiceAfter": "10s",
							"ScriptArgs": ["a", "b"],
							"Interval": "2s"
						}
					}
				}`,
		},
		hcl: []string{
			`service = {
					name = "a"
					port = 80
					enable_tag_override = true
					tagged_addresses = {
						wan = {
							address = "198.18.3.4"
							port = 443
						}
					}
					check = {
						id = "x"
						name = "y"
						DockerContainerID = "z"
						DeregisterCriticalServiceAfter = "10s"
						ScriptArgs = ["a", "b"]
						Interval = "2s"
					}
				}`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.Services = []*structs.ServiceDefinition{
				{
					Name: "a",
					Port: 80,
					TaggedAddresses: map[string]structs.ServiceAddress{
						"wan": {
							Address: "198.18.3.4",
							Port:    443,
						},
					},
					EnableTagOverride: true,
					Checks: []*structs.CheckType{
						{
							CheckID:                        "x",
							Name:                           "y",
							DockerContainerID:              "z",
							DeregisterCriticalServiceAfter: 10 * time.Second,
							ScriptArgs:                     []string{"a", "b"},
							OutputMaxSize:                  checks.DefaultBufSize,
							Interval:                       2 * time.Second,
						},
					},
					Weights: &structs.Weights{
						Passing: 1,
						Warning: 1,
					},
				},
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "ignore snapshot_agent sub-object",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`{ "snapshot_agent": { "dont": "care" } }`,
		},
		hcl: []string{
			`snapshot_agent = { dont = "care" }`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
		},
	})

	run(t, testCase{
		// Test that slices in structured config are preserved by
		// decode.HookWeakDecodeFromSlice.
		desc: "service.connectsidecar_service with checks and upstreams",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				  "service": {
						"name": "web",
						"port": 1234,
						"connect": {
							"sidecar_service": {
								"port": 2345,
								"checks": [
									{
										"TCP": "127.0.0.1:2345",
										"Interval": "10s"
									}
								],
								"proxy": {
									"expose": {
										"checks": true,
										"paths": [
											{
												"path": "/health",
												"local_path_port": 8080,
												"listener_port": 21500,
												"protocol": "http"
											}
										]
									},
									"mode": "transparent",
									"transparent_proxy": {
										"outbound_listener_port": 10101,
										"dialed_directly": true
									},
									"upstreams": [
										{
											"destination_name": "db",
											"local_bind_port": 7000
										},
										{
											"destination_name": "db2",
											"local_bind_socket_path": "/tmp/socketpath",
											"local_bind_socket_mode": "0644"
										}
									]
								}
							}
						}
					}
				}`},
		hcl: []string{`
				service {
					name = "web"
					port = 1234
					connect {
						sidecar_service {
							port = 2345
							checks = [
								{
									tcp = "127.0.0.1:2345"
									interval = "10s"
								}
							]
							proxy {
								expose {
									checks = true
									paths = [
										{
											path = "/health"
											local_path_port = 8080
											listener_port = 21500
											protocol = "http"
										}
									]
								}
								mode = "transparent"
								transparent_proxy = {
									outbound_listener_port = 10101
									dialed_directly = true
								}
								upstreams = [
									{
										destination_name = "db"
										local_bind_port = 7000
									},
									{
									    destination_name = "db2",
									    local_bind_socket_path = "/tmp/socketpath",
									    local_bind_socket_mode = "0644"
									}
								]
							}
						}
					}
				}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Services = []*structs.ServiceDefinition{
				{
					Name: "web",
					Port: 1234,
					Connect: &structs.ServiceConnect{
						SidecarService: &structs.ServiceDefinition{
							Port: 2345,
							Checks: structs.CheckTypes{
								{
									TCP:           "127.0.0.1:2345",
									Interval:      10 * time.Second,
									OutputMaxSize: checks.DefaultBufSize,
								},
							},
							Proxy: &structs.ConnectProxyConfig{
								Expose: structs.ExposeConfig{
									Checks: true,
									Paths: []structs.ExposePath{
										{
											Path:          "/health",
											LocalPathPort: 8080,
											ListenerPort:  21500,
											Protocol:      "http",
										},
									},
								},
								Mode: structs.ProxyModeTransparent,
								TransparentProxy: structs.TransparentProxyConfig{
									OutboundListenerPort: 10101,
									DialedDirectly:       true,
								},
								Upstreams: structs.Upstreams{
									structs.Upstream{
										DestinationType: "service",
										DestinationName: "db",
										LocalBindPort:   7000,
									},
									structs.Upstream{
										DestinationType:     "service",
										DestinationName:     "db2",
										LocalBindSocketPath: "/tmp/socketpath",
										LocalBindSocketMode: "0644",
									},
								},
							},
							Weights: &structs.Weights{
								Passing: 1,
								Warning: 1,
							},
						},
					},
					Weights: &structs.Weights{
						Passing: 1,
						Warning: 1,
					},
				},
			}
		},
	})
	run(t, testCase{
		// Test that slices in structured config are preserved by
		// decode.HookWeakDecodeFromSlice.
		desc: "services.connect.sidecar_service with checks and upstreams",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				  "services": [{
						"name": "web",
						"port": 1234,
						"connect": {
							"sidecar_service": {
								"port": 2345,
								"checks": [
									{
										"TCP": "127.0.0.1:2345",
										"Interval": "10s"
									}
								],
								"proxy": {
									"expose": {
										"checks": true,
										"paths": [
											{
												"path": "/health",
												"local_path_port": 8080,
												"listener_port": 21500,
												"protocol": "http"
											}
										]
									},
									"mode": "transparent",
									"transparent_proxy": {
										"outbound_listener_port": 10101,
										"dialed_directly": true
									},
									"upstreams": [
										{
											"destination_name": "db",
											"local_bind_port": 7000
										}
									]
								}
							}
						}
					}]
				}`},
		hcl: []string{`
				services = [{
					name = "web"
					port = 1234
					connect {
						sidecar_service {
							port = 2345
							checks = [
								{
									tcp = "127.0.0.1:2345"
									interval = "10s"
								}
							]
							proxy {
								expose {
									checks = true
									paths = [
										{
											path = "/health"
											local_path_port = 8080
											listener_port = 21500
											protocol = "http"
										}
									]
								}
								mode = "transparent"
								transparent_proxy = {
									outbound_listener_port = 10101
									dialed_directly = true
								}
								upstreams = [
									{
										destination_name = "db"
										local_bind_port = 7000
									},
								]
							}
						}
					}
				}]
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Services = []*structs.ServiceDefinition{
				{
					Name: "web",
					Port: 1234,
					Connect: &structs.ServiceConnect{
						SidecarService: &structs.ServiceDefinition{
							Port: 2345,
							Checks: structs.CheckTypes{
								{
									TCP:           "127.0.0.1:2345",
									Interval:      10 * time.Second,
									OutputMaxSize: checks.DefaultBufSize,
								},
							},
							Proxy: &structs.ConnectProxyConfig{
								Expose: structs.ExposeConfig{
									Checks: true,
									Paths: []structs.ExposePath{
										{
											Path:          "/health",
											LocalPathPort: 8080,
											ListenerPort:  21500,
											Protocol:      "http",
										},
									},
								},
								Mode: structs.ProxyModeTransparent,
								TransparentProxy: structs.TransparentProxyConfig{
									OutboundListenerPort: 10101,
									DialedDirectly:       true,
								},
								Upstreams: structs.Upstreams{
									structs.Upstream{
										DestinationType: "service",
										DestinationName: "db",
										LocalBindPort:   7000,
									},
								},
							},
							Weights: &structs.Weights{
								Passing: 1,
								Warning: 1,
							},
						},
					},
					Weights: &structs.Weights{
						Passing: 1,
						Warning: 1,
					},
				},
			}
		},
	})
	run(t, testCase{
		// This tests checks that VerifyServerHostname implies VerifyOutgoing
		desc: "verify_server_hostname implies verify_outgoing",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "verify_server_hostname": true
			}`},
		hcl: []string{`
			  verify_server_hostname = true
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.InternalRPC.VerifyServerHostname = true
			rt.TLS.InternalRPC.VerifyOutgoing = true
		},
		expectedWarnings: []string{
			deprecationWarning("verify_server_hostname", "tls.internal_rpc.verify_server_hostname"),
		},
	})
	run(t, testCase{
		desc: "auto_encrypt.allow_tls works implies connect",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "tls": { "internal_rpc": { "verify_incoming": true } },
			  "auto_encrypt": { "allow_tls": true },
			  "server": true
			}`},
		hcl: []string{`
			  tls { internal_rpc { verify_incoming = true } }
			  auto_encrypt { allow_tls = true }
			  server = true
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.InternalRPC.VerifyIncoming = true
			rt.AutoEncryptAllowTLS = true
			rt.ConnectEnabled = true

			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "auto_encrypt.allow_tls works with tls.defaults.verify_incoming",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "tls": { "defaults": { "verify_incoming": true } },
			  "auto_encrypt": { "allow_tls": true },
			  "server": true
			}`},
		hcl: []string{`
			  tls { defaults { verify_incoming = true } }
			  auto_encrypt { allow_tls = true }
			  server = true
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.AutoEncryptAllowTLS = true
			rt.ConnectEnabled = true

			rt.TLS.InternalRPC.VerifyIncoming = true
			rt.TLS.GRPC.VerifyIncoming = true
			rt.TLS.HTTPS.VerifyIncoming = true

			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "auto_encrypt.allow_tls works with tls.internal_rpc.verify_incoming",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "tls": { "internal_rpc": { "verify_incoming": true } },
			  "auto_encrypt": { "allow_tls": true },
			  "server": true
			}`},
		hcl: []string{`
			  tls { internal_rpc { verify_incoming = true } }
			  auto_encrypt { allow_tls = true }
			  server = true
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.AutoEncryptAllowTLS = true
			rt.ConnectEnabled = true
			rt.TLS.InternalRPC.VerifyIncoming = true

			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "auto_encrypt.allow_tls warns without tls.defaults.verify_incoming or tls.internal_rpc.verify_incoming",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "auto_encrypt": { "allow_tls": true },
			  "server": true
			}`},
		hcl: []string{`
			  auto_encrypt { allow_tls = true }
			  server = true
			`},
		expectedWarnings: []string{"if auto_encrypt.allow_tls is turned on, tls.internal_rpc.verify_incoming should be enabled (either explicitly or via tls.defaults.verify_incoming). It is necessary to turn it off during a migration to TLS, but it should definitely be turned on afterwards."},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.AutoEncryptAllowTLS = true
			rt.ConnectEnabled = true
			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "rpc.enable_streaming = true has no effect when not running in server mode",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "rpc": { "enable_streaming": true }
			}`},
		hcl: []string{`
			  rpc { enable_streaming = true }
			`},
		expectedWarnings: []string{"rpc.enable_streaming = true has no effect when not running in server mode"},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			// rpc.enable_streaming make no sense in not-server mode
			rt.RPCConfig.EnableStreaming = true
			rt.ServerMode = false
			rt.TLS.ServerMode = false
		},
	})
	run(t, testCase{
		desc: "use_streaming_backend = true requires rpc.enable_streaming on servers to work properly",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "use_streaming_backend": true,
              "rpc": {"enable_streaming": false},
			  "server": true
			}`},
		hcl: []string{`
			  use_streaming_backend = true
              rpc { enable_streaming = false }
			  server = true
			`},
		expectedWarnings: []string{"use_streaming_backend = true requires rpc.enable_streaming on servers to work properly"},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.UseStreamingBackend = true
			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	run(t, testCase{
		desc: "auto_encrypt.allow_tls errors in client mode",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "auto_encrypt": { "allow_tls": true },
			  "server": false
			}`},
		hcl: []string{`
			  auto_encrypt { allow_tls = true }
			  server = false
			`},
		expectedErr: "auto_encrypt.allow_tls can only be used on a server.",
	})
	run(t, testCase{
		desc: "auto_encrypt.tls errors in server mode",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "auto_encrypt": { "tls": true },
			  "server": true
			}`},
		hcl: []string{`
			  auto_encrypt { tls = true }
			  server = true
			`},
		expectedErr: "auto_encrypt.tls can only be used on a client.",
	})
	run(t, testCase{
		desc: "test connect vault provider configuration",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"connect": {
					"enabled": true,
					"ca_provider": "vault",
					"ca_config": {
						"ca_file": "/capath/ca.pem",
						"ca_path": "/capath/",
						"cert_file": "/certpath/cert.pem",
						"key_file": "/certpath/key.pem",
						"tls_server_name": "server.name",
						"tls_skip_verify": true,
						"token": "abc",
						"root_pki_path": "consul-vault",
						"intermediate_pki_path": "connect-intermediate"
					}
				}
			}`},
		hcl: []string{`
			  connect {
					enabled = true
					ca_provider = "vault"
					ca_config {
						ca_file = "/capath/ca.pem"
						ca_path = "/capath/"
						cert_file = "/certpath/cert.pem"
						key_file = "/certpath/key.pem"
						tls_server_name = "server.name"
						tls_skip_verify = true
						token = "abc"
						root_pki_path = "consul-vault"
						intermediate_pki_path = "connect-intermediate"
					}
				}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConnectEnabled = true
			rt.ConnectCAProvider = "vault"
			rt.ConnectCAConfig = map[string]interface{}{
				"CAFile":              "/capath/ca.pem",
				"CAPath":              "/capath/",
				"CertFile":            "/certpath/cert.pem",
				"KeyFile":             "/certpath/key.pem",
				"TLSServerName":       "server.name",
				"TLSSkipVerify":       true,
				"Token":               "abc",
				"RootPKIPath":         "consul-vault",
				"IntermediatePKIPath": "connect-intermediate",
			}
		},
	})
	run(t, testCase{
		desc: "test connect vault provider configuration with root cert ttl",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"connect": {
					"enabled": true,
					"ca_provider": "vault",
					"ca_config": {
						"ca_file": "/capath/ca.pem",
						"ca_path": "/capath/",
						"cert_file": "/certpath/cert.pem",
						"key_file": "/certpath/key.pem",
						"tls_server_name": "server.name",
						"tls_skip_verify": true,
						"token": "abc",
						"root_pki_path": "consul-vault",
						"root_cert_ttl": "96360h",
						"intermediate_pki_path": "connect-intermediate"
					}
				}
			}`},
		hcl: []string{`
			  connect {
					enabled = true
					ca_provider = "vault"
					ca_config {
						ca_file = "/capath/ca.pem"
						ca_path = "/capath/"
						cert_file = "/certpath/cert.pem"
						key_file = "/certpath/key.pem"
						tls_server_name = "server.name"
						tls_skip_verify = true
						root_pki_path = "consul-vault"
						token = "abc"
						intermediate_pki_path = "connect-intermediate"
						root_cert_ttl = "96360h"
					}
				}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConnectEnabled = true
			rt.ConnectCAProvider = "vault"
			rt.ConnectCAConfig = map[string]interface{}{
				"CAFile":              "/capath/ca.pem",
				"CAPath":              "/capath/",
				"CertFile":            "/certpath/cert.pem",
				"KeyFile":             "/certpath/key.pem",
				"TLSServerName":       "server.name",
				"TLSSkipVerify":       true,
				"Token":               "abc",
				"RootPKIPath":         "consul-vault",
				"RootCertTTL":         "96360h",
				"IntermediatePKIPath": "connect-intermediate",
			}
		},
	})
	run(t, testCase{
		desc: "Connect AWS CA provider configuration",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"connect": {
					"enabled": true,
					"ca_provider": "aws-pca",
					"ca_config": {
						"existing_arn": "foo",
						"delete_on_exit": true
					}
				}
			}`},
		hcl: []string{`
			  connect {
					enabled = true
					ca_provider = "aws-pca"
					ca_config {
						existing_arn = "foo"
						delete_on_exit = true
					}
				}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConnectEnabled = true
			rt.ConnectCAProvider = "aws-pca"
			rt.ConnectCAConfig = map[string]interface{}{
				"ExistingARN":  "foo",
				"DeleteOnExit": true,
			}
		},
	})
	run(t, testCase{
		desc: "Connect AWS CA provider TTL validation",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"connect": {
					"enabled": true,
					"ca_provider": "aws-pca",
					"ca_config": {
						"leaf_cert_ttl": "1h"
					}
				}
			}`},
		hcl: []string{`
			  connect {
					enabled = true
					ca_provider = "aws-pca"
					ca_config {
						leaf_cert_ttl = "1h"
					}
				}
			`},
		expectedErr: "AWS PCA doesn't support certificates that are valid for less than 24 hours",
	})
	run(t, testCase{
		desc: "Connect AWS CA provider EC key length validation",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
				"connect": {
					"enabled": true,
					"ca_provider": "aws-pca",
					"ca_config": {
						"private_key_bits": 521
					}
				}
			}`},
		hcl: []string{`
			  connect {
					enabled = true
					ca_provider = "aws-pca"
					ca_config {
						private_key_bits = 521
					}
				}
			`},
		expectedErr: "AWS PCA only supports P256 EC curve",
	})
	run(t, testCase{
		desc: "connect.enable_mesh_gateway_wan_federation requires connect.enabled",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "connect": {
				"enabled": false,
				"enable_mesh_gateway_wan_federation": true
			  }
			}`},
		hcl: []string{`
			  connect {
			    enabled = false
			    enable_mesh_gateway_wan_federation = true
			  }
			`},
		expectedErr: "'connect.enable_mesh_gateway_wan_federation=true' requires 'connect.enabled=true'",
	})
	run(t, testCase{
		desc: "connect.enable_mesh_gateway_wan_federation cannot use -join-wan",
		args: []string{
			`-data-dir=` + dataDir,
			`-join-wan=1.2.3.4`,
		},
		json: []string{`{
			  "server": true,
			  "primary_datacenter": "one",
			  "datacenter": "one",
			  "connect": {
				"enabled": true,
				"enable_mesh_gateway_wan_federation": true
			  }
			}`},
		hcl: []string{`
			  server = true
			  primary_datacenter = "one"
			  datacenter = "one"
			  connect {
			    enabled = true
			    enable_mesh_gateway_wan_federation = true
			  }
			`},
		expectedErr: "'retry_join_wan' is incompatible with 'connect.enable_mesh_gateway_wan_federation = true'",
		expectedWarnings: []string{
			deprecatedFlagWarning("-join-wan", "-retry-join-wan"),
		},
	})
	run(t, testCase{
		desc: "connect.enable_mesh_gateway_wan_federation cannot use -retry-join-wan",
		args: []string{
			`-data-dir=` + dataDir,
			`-retry-join-wan=1.2.3.4`,
		},
		json: []string{`{
			  "server": true,
			  "primary_datacenter": "one",
			  "datacenter": "one",
			  "connect": {
				"enabled": true,
				"enable_mesh_gateway_wan_federation": true
			  }
			}`},
		hcl: []string{`
			  server = true
			  primary_datacenter = "one"
			  datacenter = "one"
			  connect {
			    enabled = true
			    enable_mesh_gateway_wan_federation = true
			  }
			`},
		expectedErr: "'retry_join_wan' is incompatible with 'connect.enable_mesh_gateway_wan_federation = true'",
	})
	run(t, testCase{
		desc: "connect.enable_mesh_gateway_wan_federation requires server mode",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "server": false,
			  "connect": {
				"enabled": true,
				"enable_mesh_gateway_wan_federation": true
			  }
			}`},
		hcl: []string{`
			  server = false
			  connect {
			    enabled = true
			    enable_mesh_gateway_wan_federation = true
			  }
			`},
		expectedErr: "'connect.enable_mesh_gateway_wan_federation = true' requires 'server = true'",
	})
	run(t, testCase{
		desc: "connect.enable_mesh_gateway_wan_federation requires no slashes in node names",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "server": true,
			  "node_name": "really/why",
			  "connect": {
				"enabled": true,
				"enable_mesh_gateway_wan_federation": true
			  }
			}`},
		hcl: []string{`
			  server = true
			  node_name = "really/why"
			  connect {
			    enabled = true
			    enable_mesh_gateway_wan_federation = true
			  }
			`},
		expectedErr: "'connect.enable_mesh_gateway_wan_federation = true' requires that 'node_name' not contain '/' characters",
	})
	run(t, testCase{
		desc: "primary_gateways requires server mode",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "server": false,
			  "primary_gateways": [ "foo.local", "bar.local" ]
			}`},
		hcl: []string{`
			  server = false
			  primary_gateways = [ "foo.local", "bar.local" ]
			`},
		expectedErr: "'primary_gateways' requires 'server = true'",
	})
	run(t, testCase{
		desc: "primary_gateways only works in a secondary datacenter",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "server": true,
			  "primary_datacenter": "one",
			  "datacenter": "one",
			  "primary_gateways": [ "foo.local", "bar.local" ]
			}`},
		hcl: []string{`
			  server = true
			  primary_datacenter = "one"
			  datacenter = "one"
			  primary_gateways = [ "foo.local", "bar.local" ]
			`},
		expectedErr: "'primary_gateways' should only be configured in a secondary datacenter",
	})
	run(t, testCase{
		desc: "connect.enable_mesh_gateway_wan_federation in secondary with primary_gateways configured",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			  "server": true,
			  "primary_datacenter": "one",
			  "datacenter": "two",
			  "primary_gateways": [ "foo.local", "bar.local" ],
			  "connect": {
				"enabled": true,
				"enable_mesh_gateway_wan_federation": true
			  }
			}`},
		hcl: []string{`
			  server = true
			  primary_datacenter = "one"
			  datacenter = "two"
			  primary_gateways = [ "foo.local", "bar.local" ]
			  connect {
			    enabled = true
			    enable_mesh_gateway_wan_federation = true
			  }
			`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Datacenter = "two"
			rt.PrimaryDatacenter = "one"
			rt.PrimaryGateways = []string{"foo.local", "bar.local"}
			rt.ConnectEnabled = true
			rt.ConnectMeshGatewayWANFederationEnabled = true
			// server things
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.LeaveOnTerm = false
			rt.SkipLeaveOnInt = true
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})

	// ------------------------------------------------------------
	// ConfigEntry Handling
	//
	run(t, testCase{
		desc: "ConfigEntry bootstrap doesn't parse",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"foo": "bar"
						}
					]
				}
			}`},
		hcl: []string{`
			config_entries {
				bootstrap {
					foo = "bar"
				}
			}`},
		expectedErr: "config_entries.bootstrap[0]: Payload does not contain a kind/Kind",
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap unknown kind",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "foo",
							"name": "bar",
							"baz": 1
						}
					]
				}
			}`},
		hcl: []string{`
			config_entries {
				bootstrap {
					kind = "foo"
					name = "bar"
					baz = 1
				}
			}`},
		expectedErr: "config_entries.bootstrap[0]: invalid config entry kind: foo",
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap invalid service-defaults",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "service-defaults",
							"name": "web",
							"made_up_key": "blah"
						}
					]
				}
			}`},
		hcl: []string{`
			config_entries {
				bootstrap {
					kind = "service-defaults"
					name = "web"
					made_up_key = "blah"
				}
			}`},
		expectedErr: "config_entries.bootstrap[0]: 1 error occurred:\n\t* invalid config key \"made_up_key\"\n\n",
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap proxy-defaults (snake-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "proxy-defaults",
							"name": "global",
							"config": {
								"bar": "abc",
								"moreconfig": {
									"moar": "config"
								}
							},
							"mesh_gateway": {
								"mode": "remote"
							},
							"mode": "transparent",
							"transparent_proxy": {
								"outbound_listener_port": 10101,
								"dialed_directly": true
							}
						}
					]
				}
			}`},
		hcl: []string{`
				config_entries {
					bootstrap {
						kind = "proxy-defaults"
						name = "global"
						config {
						  "bar" = "abc"
						  "moreconfig" {
							"moar" = "config"
						  }
						}
						mesh_gateway {
							mode = "remote"
						}
						mode = "transparent"
						transparent_proxy = {
							outbound_listener_port = 10101
							dialed_directly = true
						}
					}
				}`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ProxyConfigEntry{
					Kind:           structs.ProxyDefaults,
					Name:           structs.ProxyConfigGlobal,
					EnterpriseMeta: *defaultEntMeta,
					Config: map[string]interface{}{
						"bar": "abc",
						"moreconfig": map[string]interface{}{
							"moar": "config",
						},
					},
					MeshGateway: structs.MeshGatewayConfig{
						Mode: structs.MeshGatewayModeRemote,
					},
					Mode: structs.ProxyModeTransparent,
					TransparentProxy: structs.TransparentProxyConfig{
						OutboundListenerPort: 10101,
						DialedDirectly:       true,
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap proxy-defaults (camel-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"Kind": "proxy-defaults",
							"Name": "global",
							"Config": {
								"bar": "abc",
								"moreconfig": {
									"moar": "config"
								}
							},
							"MeshGateway": {
								"Mode": "remote"
							},
							"Mode": "transparent",
							"TransparentProxy": {
								"OutboundListenerPort": 10101,
								"DialedDirectly": true
							}
						}
					]
				}
			}`},
		hcl: []string{`
				config_entries {
					bootstrap {
						Kind = "proxy-defaults"
						Name = "global"
						Config {
						  "bar" = "abc"
						  "moreconfig" {
							"moar" = "config"
						  }
						}
						MeshGateway {
							Mode = "remote"
						}
						Mode = "transparent"
						TransparentProxy = {
							OutboundListenerPort = 10101
							DialedDirectly = true
						}
					}
				}`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ProxyConfigEntry{
					Kind:           structs.ProxyDefaults,
					Name:           structs.ProxyConfigGlobal,
					EnterpriseMeta: *defaultEntMeta,
					Config: map[string]interface{}{
						"bar": "abc",
						"moreconfig": map[string]interface{}{
							"moar": "config",
						},
					},
					MeshGateway: structs.MeshGatewayConfig{
						Mode: structs.MeshGatewayModeRemote,
					},
					Mode: structs.ProxyModeTransparent,
					TransparentProxy: structs.TransparentProxyConfig{
						OutboundListenerPort: 10101,
						DialedDirectly:       true,
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap service-defaults (snake-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "service-defaults",
							"name": "web",
							"meta" : {
								"foo": "bar",
								"gir": "zim"
							},
							"protocol": "http",
							"external_sni": "abc-123",
							"mesh_gateway": {
								"mode": "remote"
							},
							"mode": "transparent",
							"transparent_proxy": {
								"outbound_listener_port": 10101,
								"dialed_directly": true
							}
						}
					]
				}
			}`},
		hcl: []string{`
				config_entries {
					bootstrap {
						kind = "service-defaults"
						name = "web"
						meta {
							"foo" = "bar"
							"gir" = "zim"
						}
						protocol = "http"
						external_sni = "abc-123"
						mesh_gateway {
							mode = "remote"
						}
						mode = "transparent"
						transparent_proxy = {
							outbound_listener_port = 10101
							dialed_directly = true
						}
					}
				}`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ServiceConfigEntry{
					Kind: structs.ServiceDefaults,
					Name: "web",
					Meta: map[string]string{
						"foo": "bar",
						"gir": "zim",
					},
					EnterpriseMeta: *defaultEntMeta,
					Protocol:       "http",
					ExternalSNI:    "abc-123",
					MeshGateway: structs.MeshGatewayConfig{
						Mode: structs.MeshGatewayModeRemote,
					},
					Mode: structs.ProxyModeTransparent,
					TransparentProxy: structs.TransparentProxyConfig{
						OutboundListenerPort: 10101,
						DialedDirectly:       true,
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap service-defaults (camel-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"Kind": "service-defaults",
							"Name": "web",
							"Meta" : {
								"foo": "bar",
								"gir": "zim"
							},
							"Protocol": "http",
							"ExternalSNI": "abc-123",
							"MeshGateway": {
								"Mode": "remote"
							},
							"Mode": "transparent",
							"TransparentProxy": {
								"OutboundListenerPort": 10101,
								"DialedDirectly": true
							}
						}
					]
				}
			}`},
		hcl: []string{`
				config_entries {
					bootstrap {
						Kind = "service-defaults"
						Name = "web"
						Meta {
							"foo" = "bar"
							"gir" = "zim"
						}
						Protocol = "http"
						ExternalSNI = "abc-123"
						MeshGateway {
							Mode = "remote"
						}
						Mode = "transparent"
						TransparentProxy = {
							OutboundListenerPort = 10101
							DialedDirectly = true
						}
					}
				}`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ServiceConfigEntry{
					Kind: structs.ServiceDefaults,
					Name: "web",
					Meta: map[string]string{
						"foo": "bar",
						"gir": "zim",
					},
					EnterpriseMeta: *defaultEntMeta,
					Protocol:       "http",
					ExternalSNI:    "abc-123",
					MeshGateway: structs.MeshGatewayConfig{
						Mode: structs.MeshGatewayModeRemote,
					},
					Mode: structs.ProxyModeTransparent,
					TransparentProxy: structs.TransparentProxyConfig{
						OutboundListenerPort: 10101,
						DialedDirectly:       true,
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap service-router (snake-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "service-router",
							"name": "main",
							"meta" : {
								"foo": "bar",
								"gir": "zim"
							},
							"routes": [
								{
									"match": {
										"http": {
											"path_exact": "/foo",
											"header": [
												{
													"name": "debug1",
													"present": true
												},
												{
													"name": "debug2",
													"present": true,
													"invert": true
												},
												{
													"name": "debug3",
													"exact": "1"
												},
												{
													"name": "debug4",
													"prefix": "aaa"
												},
												{
													"name": "debug5",
													"suffix": "bbb"
												},
												{
													"name": "debug6",
													"regex": "a.*z"
												}
											]
										}
									},
									"destination": {
									  "service"                 : "carrot",
									  "service_subset"          : "kale",
									  "namespace"               : "leek",
									  "prefix_rewrite"          : "/alternate",
									  "request_timeout"         : "99s",
									  "idle_timeout"            : "99s",
									  "num_retries"             : 12345,
									  "retry_on_connect_failure": true,
									  "retry_on_status_codes"   : [401, 209]
									}
								},
								{
									"match": {
										"http": {
											"path_prefix": "/foo",
											"methods": [ "GET", "DELETE" ],
											"query_param": [
												{
													"name": "hack1",
													"present": true
												},
												{
													"name": "hack2",
													"exact": "1"
												},
												{
													"name": "hack3",
													"regex": "a.*z"
												}
											]
										}
									}
								},
								{
									"match": {
										"http": {
											"path_regex": "/foo"
										}
									}
								}
							]
						}
					]
				}
			}`},
		hcl: []string{`
				config_entries {
					bootstrap {
						kind = "service-router"
						name = "main"
						meta {
							"foo" = "bar"
							"gir" = "zim"
						}
						routes = [
							{
								match {
									http {
										path_exact = "/foo"
										header = [
											{
												name = "debug1"
												present = true
											},
											{
												name = "debug2"
												present = true
												invert = true
											},
											{
												name = "debug3"
												exact = "1"
											},
											{
												name = "debug4"
												prefix = "aaa"
											},
											{
												name = "debug5"
												suffix = "bbb"
											},
											{
												name = "debug6"
												regex = "a.*z"
											},
										]
									}
								}
								destination {
								  service               = "carrot"
								  service_subset         = "kale"
								  namespace             = "leek"
								  prefix_rewrite         = "/alternate"
								  request_timeout        = "99s"
								  idle_timeout           = "99s"
								  num_retries            = 12345
								  retry_on_connect_failure = true
								  retry_on_status_codes    = [401, 209]
								}
							},
							{
								match {
									http {
										path_prefix = "/foo"
										methods = [ "GET", "DELETE" ]
										query_param = [
											{
												name = "hack1"
												present = true
											},
											{
												name = "hack2"
												exact = "1"
											},
											{
												name = "hack3"
												regex = "a.*z"
											},
										]
									}
								}
							},
							{
								match {
									http {
										path_regex = "/foo"
									}
								}
							},
						]
					}
				}`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ServiceRouterConfigEntry{
					Kind: structs.ServiceRouter,
					Name: "main",
					Meta: map[string]string{
						"foo": "bar",
						"gir": "zim",
					},
					EnterpriseMeta: *defaultEntMeta,
					Routes: []structs.ServiceRoute{
						{
							Match: &structs.ServiceRouteMatch{
								HTTP: &structs.ServiceRouteHTTPMatch{
									PathExact: "/foo",
									Header: []structs.ServiceRouteHTTPMatchHeader{
										{
											Name:    "debug1",
											Present: true,
										},
										{
											Name:    "debug2",
											Present: true,
											Invert:  true,
										},
										{
											Name:  "debug3",
											Exact: "1",
										},
										{
											Name:   "debug4",
											Prefix: "aaa",
										},
										{
											Name:   "debug5",
											Suffix: "bbb",
										},
										{
											Name:  "debug6",
											Regex: "a.*z",
										},
									},
								},
							},
							Destination: &structs.ServiceRouteDestination{
								Service:               "carrot",
								ServiceSubset:         "kale",
								Namespace:             "leek",
								Partition:             acl.DefaultPartitionName,
								PrefixRewrite:         "/alternate",
								RequestTimeout:        99 * time.Second,
								IdleTimeout:           99 * time.Second,
								NumRetries:            12345,
								RetryOnConnectFailure: true,
								RetryOnStatusCodes:    []uint32{401, 209},
							},
						},
						{
							Match: &structs.ServiceRouteMatch{
								HTTP: &structs.ServiceRouteHTTPMatch{
									PathPrefix: "/foo",
									Methods:    []string{"GET", "DELETE"},
									QueryParam: []structs.ServiceRouteHTTPMatchQueryParam{
										{
											Name:    "hack1",
											Present: true,
										},
										{
											Name:  "hack2",
											Exact: "1",
										},
										{
											Name:  "hack3",
											Regex: "a.*z",
										},
									},
								},
							},
						},
						{
							Match: &structs.ServiceRouteMatch{
								HTTP: &structs.ServiceRouteHTTPMatch{
									PathRegex: "/foo",
								},
							},
						},
					},
				},
			}
		},
	})
	// TODO(rb): add in missing tests for ingress-gateway (snake + camel)
	// TODO(rb): add in missing tests for terminating-gateway (snake + camel)
	run(t, testCase{
		desc: "ConfigEntry bootstrap service-intentions (snake-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "service-intentions",
							"name": "web",
							"meta" : {
								"foo": "bar",
								"gir": "zim"
							},
							"sources": [
								{
									"name": "foo",
									"action": "deny",
									"type": "consul",
									"description": "foo desc"
								},
								{
									"name": "bar",
									"action": "allow",
									"description": "bar desc"
								},
								{
									"name": "*",
									"action": "deny",
									"description": "wild desc"
								}
							]
						}
					]
				}
			}`,
		},
		hcl: []string{`
				config_entries {
				  bootstrap {
					kind = "service-intentions"
					name = "web"
					meta {
						"foo" = "bar"
						"gir" = "zim"
					}
					sources = [
					  {
						name        = "foo"
						action      = "deny"
						type        = "consul"
						description = "foo desc"
					  },
					  {
						name        = "bar"
						action      = "allow"
						description = "bar desc"
					  }
					]
					sources {
					  name        = "*"
					  action      = "deny"
					  description = "wild desc"
					}
				  }
				}
			`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ServiceIntentionsConfigEntry{
					Kind: "service-intentions",
					Name: "web",
					Meta: map[string]string{
						"foo": "bar",
						"gir": "zim",
					},
					EnterpriseMeta: *defaultEntMeta,
					Sources: []*structs.SourceIntention{
						{
							Name:           "foo",
							Action:         "deny",
							Type:           "consul",
							Description:    "foo desc",
							Precedence:     9,
							EnterpriseMeta: *defaultEntMeta,
						},
						{
							Name:           "bar",
							Action:         "allow",
							Type:           "consul",
							Description:    "bar desc",
							Precedence:     9,
							EnterpriseMeta: *defaultEntMeta,
						},
						{
							Name:           "*",
							Action:         "deny",
							Type:           "consul",
							Description:    "wild desc",
							Precedence:     8,
							EnterpriseMeta: *defaultEntMeta,
						},
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap service-intentions wildcard destination (snake-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "service-intentions",
							"name": "*",
							"sources": [
								{
									"name": "foo",
									"action": "deny",
									"precedence": 6
								}
							]
						}
					]
				}
			}`,
		},
		hcl: []string{`
				config_entries {
				  bootstrap {
					kind = "service-intentions"
					name = "*"
					sources {
					  name   = "foo"
					  action = "deny"
					  # should be parsed, but we'll ignore it later
					  precedence = 6
					}
				  }
				}
			`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.ServiceIntentionsConfigEntry{
					Kind:           "service-intentions",
					Name:           "*",
					EnterpriseMeta: *defaultEntMeta,
					Sources: []*structs.SourceIntention{
						{
							Name:           "foo",
							Action:         "deny",
							Type:           "consul",
							Precedence:     6,
							EnterpriseMeta: *defaultEntMeta,
						},
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap cluster (snake-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"kind": "mesh",
							"meta" : {
								"foo": "bar",
								"gir": "zim"
							},
							"transparent_proxy": {
								"mesh_destinations_only": true
							}
						}
					]
				}
			}`,
		},
		hcl: []string{`
				config_entries {
				  bootstrap {
					kind = "mesh"
					meta {
						"foo" = "bar"
						"gir" = "zim"
					}
					transparent_proxy {
						mesh_destinations_only = true
					}
				  }
				}
			`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.MeshConfigEntry{
					Meta: map[string]string{
						"foo": "bar",
						"gir": "zim",
					},
					EnterpriseMeta: *defaultEntMeta,
					TransparentProxy: structs.TransparentProxyMeshConfig{
						MeshDestinationsOnly: true,
					},
				},
			}
		},
	})
	run(t, testCase{
		desc: "ConfigEntry bootstrap cluster (camel-case)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"config_entries": {
					"bootstrap": [
						{
							"Kind": "mesh",
							"Meta" : {
								"foo": "bar",
								"gir": "zim"
							},
							"TransparentProxy": {
								"MeshDestinationsOnly": true
							}
						}
					]
				}
			}`,
		},
		hcl: []string{`
				config_entries {
				  bootstrap {
					Kind = "mesh"
					Meta {
						"foo" = "bar"
						"gir" = "zim"
					}
					TransparentProxy {
						MeshDestinationsOnly = true
					}
				  }
				}
			`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.ConfigEntryBootstrap = []structs.ConfigEntry{
				&structs.MeshConfigEntry{
					Meta: map[string]string{
						"foo": "bar",
						"gir": "zim",
					},
					EnterpriseMeta: *defaultEntMeta,
					TransparentProxy: structs.TransparentProxyMeshConfig{
						MeshDestinationsOnly: true,
					},
				},
			}
		},
	})

	// /////////////////////////////////
	// Defaults sanity checks

	run(t, testCase{
		desc: "default limits",
		args: []string{
			`-data-dir=` + dataDir,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			// Note that in the happy case this test will pass even if you comment
			// out all the stuff below since rt is also initialized from the
			// defaults. But it's still valuable as it will fail as soon as the
			// defaults are changed from these values forcing that change to be
			// intentional.
			rt.RPCHandshakeTimeout = 5 * time.Second
			rt.RPCClientTimeout = 60 * time.Second
			rt.HTTPSHandshakeTimeout = 5 * time.Second
			rt.HTTPMaxConnsPerClient = 200
			rt.RPCMaxConnsPerClient = 100
			rt.RequestLimitsMode = consulrate.ModeDisabled
			rt.RequestLimitsReadRate = rate.Inf
			rt.RequestLimitsWriteRate = rate.Inf
			rt.SegmentLimit = 64
			rt.XDSUpdateRateLimit = 250
			rt.RPCRateLimit = rate.Inf
			rt.RPCMaxBurst = 1000
		},
	})

	// /////////////////////////////////
	// Auto Config related tests
	run(t, testCase{
		desc: "auto config and auto encrypt error",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				auto_config {
					enabled = true
					intro_token = "blah"
					server_addresses = ["198.18.0.1"]
				}
				auto_encrypt {
					tls = true
				}
				tls {
					internal_rpc {
						verify_outgoing = true
					}
				}
			`},
		json: []string{`{
				"auto_config": {
					"enabled": true,
					"intro_token": "blah",
					"server_addresses": ["198.18.0.1"]
				},
				"auto_encrypt": {
					"tls": true
				},
				"tls": {
					"internal_rpc": {
						"verify_outgoing": true
					}
				}
			}`},
		expectedErr: "both auto_encrypt.tls and auto_config.enabled cannot be set to true.",
	})
	run(t, testCase{
		desc: "auto config not allowed for servers",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				server = true
				auto_config {
					enabled = true
					intro_token = "blah"
					server_addresses = ["198.18.0.1"]
				}
				tls {
					internal_rpc {
						verify_outgoing = true
					}
				}
			`},
		json: []string{`
			{
				"server": true,
				"auto_config": {
					"enabled": true,
					"intro_token": "blah",
					"server_addresses": ["198.18.0.1"]
				},
				"tls": {
					"internal_rpc": {
						"verify_outgoing": true
					}
				}
			}`},
		expectedErr: "auto_config.enabled cannot be set to true for server agents",
	})

	run(t, testCase{
		desc: "auto config tls not enabled",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				auto_config {
					enabled = true
					server_addresses = ["198.18.0.1"]
					intro_token = "foo"
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"enabled": true,
					"server_addresses": ["198.18.0.1"],
					"intro_token": "foo"
				}
			}`},
		expectedErr: "auto_config.enabled cannot be set without configuring TLS for server communications",
	})

	run(t, testCase{
		desc: "auto config server tls not enabled",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				server = true
				auto_config {
					authorization {
						enabled = true
					}
				}
			`},
		json: []string{`
			{
				"server": true,
				"auto_config": {
					"authorization": {
						"enabled": true
					}
				}
			}`},
		expectedErr: "auto_config.authorization.enabled cannot be set without providing a TLS certificate for the server",
	})

	run(t, testCase{
		desc: "auto config no intro token",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				auto_config {
					enabled = true
				 	server_addresses = ["198.18.0.1"]
				}
				tls {
					internal_rpc {
						verify_outgoing = true
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"enabled": true,
					"server_addresses": ["198.18.0.1"]
				},
				"tls": {
					"internal_rpc": {
						"verify_outgoing": true
					}
				}
			}`},
		expectedErr: "One of auto_config.intro_token, auto_config.intro_token_file or the CONSUL_INTRO_TOKEN environment variable must be set to enable auto_config",
	})

	run(t, testCase{
		desc: "auto config no server addresses",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				auto_config {
					enabled = true
					intro_token = "blah"
				}
				tls {
					internal_rpc {
						verify_outgoing = true
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"enabled": true,
					"intro_token": "blah"
				},
				"tls": {
					"internal_rpc": {
						"verify_outgoing": true
					}
				}
			}`},
		expectedErr: "auto_config.enabled is set without providing a list of addresses",
	})

	run(t, testCase{
		desc: "auto config client",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				auto_config {
					enabled = true
					intro_token = "blah"
					intro_token_file = "blah"
					server_addresses = ["198.18.0.1"]
					dns_sans = ["foo"]
					ip_sans = ["invalid", "127.0.0.1"]
				}
				tls {
					internal_rpc {
						verify_outgoing = true
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"enabled": true,
					"intro_token": "blah",
					"intro_token_file": "blah",
					"server_addresses": ["198.18.0.1"],
					"dns_sans": ["foo"],
					"ip_sans": ["invalid", "127.0.0.1"]
				},
				"tls": {
					"internal_rpc": {
						"verify_outgoing": true
					}
				}
			}`},
		expectedWarnings: []string{
			"Cannot parse ip \"invalid\" from auto_config.ip_sans",
			"Both an intro token and intro token file are set. The intro token will be used instead of the file",
		},
		expected: func(rt *RuntimeConfig) {
			rt.ConnectEnabled = true
			rt.AutoConfig.Enabled = true
			rt.AutoConfig.IntroToken = "blah"
			rt.AutoConfig.IntroTokenFile = "blah"
			rt.AutoConfig.ServerAddresses = []string{"198.18.0.1"}
			rt.AutoConfig.DNSSANs = []string{"foo"}
			rt.AutoConfig.IPSANs = []net.IP{net.IPv4(127, 0, 0, 1)}
			rt.DataDir = dataDir
			rt.TLS.InternalRPC.VerifyOutgoing = true
			rt.TLS.AutoTLS = true
		},
	})

	run(t, testCase{
		desc: "auto config authorizer client not allowed",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
				auto_config {
					authorization {
						enabled = true
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"authorization": {
						"enabled": true
					}
				}
			}`},
		expectedErr: "auto_config.authorization.enabled cannot be set to true for client agents",
	})

	run(t, testCase{
		desc: "auto config authorizer invalid config",
		args: []string{
			`-data-dir=` + dataDir,
			`-server`,
		},
		hcl: []string{`
				auto_config {
					authorization {
						enabled = true
					}
				}
				tls {
					internal_rpc {
						cert_file = "foo"
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"authorization": {
						"enabled": true
					}
				},
				"tls": {
					"internal_rpc": {
						"cert_file": "foo"
					}
				}
			}`},
		expectedErr: `auto_config.authorization.static has invalid configuration: exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type "jwt"`,
	})

	run(t, testCase{
		desc: "auto config authorizer invalid config 2",
		args: []string{
			`-data-dir=` + dataDir,
			`-server`,
		},
		hcl: []string{`
				auto_config {
					authorization {
						enabled = true
						static {
							jwks_url = "https://fake.uri.local"
							oidc_discovery_url = "https://fake.uri.local"
						}
					}
				}
				tls {
					internal_rpc {
						cert_file = "foo"
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"authorization": {
						"enabled": true,
						"static": {
							"jwks_url": "https://fake.uri.local",
							"oidc_discovery_url": "https://fake.uri.local"
						}
					}
				},
				"tls": {
					"internal_rpc": {
						"cert_file": "foo"
					}
				}
			}`},
		expectedErr: `auto_config.authorization.static has invalid configuration: exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type "jwt"`,
	})

	run(t, testCase{
		desc: "auto config authorizer require token replication in secondary",
		args: []string{
			`-data-dir=` + dataDir,
			`-server`,
		},
		hcl: []string{`
				primary_datacenter = "otherdc"
				acl {
					enabled = true
				}
				auto_config {
					authorization {
						enabled = true
						static {
							jwks_url = "https://fake.uri.local"
							oidc_discovery_url = "https://fake.uri.local"
						}
					}
				}
				tls {
					internal_rpc {
						cert_file = "foo"
					}
				}
			`},
		json: []string{`
			{
				"primary_datacenter": "otherdc",
				"acl": {
					"enabled": true
				},
				"auto_config": {
					"authorization": {
						"enabled": true,
						"static": {
							"jwks_url": "https://fake.uri.local",
							"oidc_discovery_url": "https://fake.uri.local"
						}
					}
				},
				"tls": {
					"internal_rpc": {
						"cert_file": "foo"
					}
				}
			}`},
		expectedErr: `Enabling auto-config authorization (auto_config.authorization.enabled) in non primary datacenters with ACLs enabled (acl.enabled) requires also enabling ACL token replication (acl.enable_token_replication)`,
	})

	run(t, testCase{
		desc: "auto config authorizer invalid claim assertion",
		args: []string{
			`-data-dir=` + dataDir,
			`-server`,
		},
		hcl: []string{`
				auto_config {
					authorization {
						enabled = true
						static {
							jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"]
							claim_assertions = [
								"values.node == ${node}"
							]
						}
					}
				}
				tls {
					internal_rpc {
						cert_file = "foo"
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"authorization": {
						"enabled": true,
						"static": {
							"jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"],
							"claim_assertions": [
								"values.node == ${node}"
							]
						}
					}
				},
				"tls": {
					"internal_rpc": {
						"cert_file": "foo"
					}
				}
			}`},
		expectedErr: `auto_config.authorization.static.claim_assertion "values.node == ${node}" is invalid: Selector "values" is not valid`,
	})
	run(t, testCase{
		desc: "auto config authorizer ok",
		args: []string{
			`-data-dir=` + dataDir,
			`-server`,
		},
		hcl: []string{`
				auto_config {
					authorization {
						enabled = true
						static {
							jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"]
							claim_assertions = [
								"value.node == ${node}"
							]
							claim_mappings = {
								node = "node"
							}
						}
					}
				}
				tls {
					internal_rpc {
						cert_file = "foo"
					}
				}
			`},
		json: []string{`
			{
				"auto_config": {
					"authorization": {
						"enabled": true,
						"static": {
							"jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"],
							"claim_assertions": [
								"value.node == ${node}"
							],
							"claim_mappings": {
								"node": "node"
							}
						}
					}
				},
				"tls": {
					"internal_rpc": {
						"cert_file": "foo"
					}
				}
			}`},
		expected: func(rt *RuntimeConfig) {
			rt.AutoConfig.Authorizer.Enabled = true
			rt.AutoConfig.Authorizer.AuthMethod.Config["ClaimMappings"] = map[string]string{
				"node": "node",
			}
			rt.AutoConfig.Authorizer.AuthMethod.Config["JWTValidationPubKeys"] = []string{"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"}
			rt.AutoConfig.Authorizer.ClaimAssertions = []string{"value.node == ${node}"}
			rt.DataDir = dataDir
			rt.LeaveOnTerm = false
			rt.ServerMode = true
			rt.TLS.ServerMode = true
			rt.SkipLeaveOnInt = true
			rt.TLS.InternalRPC.CertFile = "foo"
			rt.RPCConfig.EnableStreaming = true
			rt.GRPCTLSPort = 8503
			rt.GRPCTLSAddrs = []net.Addr{defaultGrpcTlsAddr}
		},
	})
	// UI Config tests
	run(t, testCase{
		desc: "ui config deprecated",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui": true,
				"ui_content_path": "/bar"
			}`},
		hcl: []string{`
			ui = true
			ui_content_path = "/bar"
			`},
		expectedWarnings: []string{
			`The 'ui' field is deprecated. Use the 'ui_config.enabled' field instead.`,
			`The 'ui_content_path' field is deprecated. Use the 'ui_config.content_path' field instead.`,
		},
		expected: func(rt *RuntimeConfig) {
			// Should still work!
			rt.UIConfig.Enabled = true
			rt.UIConfig.ContentPath = "/bar/"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "ui-dir config deprecated",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_dir": "/bar"
			}`},
		hcl: []string{`
			ui_dir = "/bar"
			`},
		expectedWarnings: []string{
			`The 'ui_dir' field is deprecated. Use the 'ui_config.dir' field instead.`,
		},
		expected: func(rt *RuntimeConfig) {
			// Should still work!
			rt.UIConfig.Dir = "/bar"
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "metrics_provider constraint",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_provider": "((((lisp 4 life))))"
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_provider = "((((lisp 4 life))))"
			}
			`},
		expectedErr: `ui_config.metrics_provider can only contain lowercase alphanumeric, - or _ characters.`,
	})
	run(t, testCase{
		desc: "metrics_provider_options_json invalid JSON",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_provider_options_json": "not valid JSON"
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_provider_options_json = "not valid JSON"
			}
			`},
		expectedErr: `ui_config.metrics_provider_options_json must be empty or a string containing a valid JSON object.`,
	})
	run(t, testCase{
		desc: "metrics_provider_options_json not an object",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_provider_options_json": "1.0"
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_provider_options_json = "1.0"
			}
			`},
		expectedErr: `ui_config.metrics_provider_options_json must be empty or a string containing a valid JSON object.`,
	})
	run(t, testCase{
		desc: "metrics_proxy.base_url valid",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"base_url": "___"
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					base_url = "___"
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist invalid (empty)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["", "/foo"]
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.path_allowlist: path "" is not an absolute path`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist invalid (relative)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["bar/baz", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["bar/baz", "/foo"]
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.path_allowlist: path "bar/baz" is not an absolute path`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist invalid (weird)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["://bar/baz", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["://bar/baz", "/foo"]
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.path_allowlist: path "://bar/baz" is not an absolute path`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist invalid (fragment)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["/bar/baz#stuff", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["/bar/baz#stuff", "/foo"]
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.path_allowlist: path "/bar/baz#stuff" is not an absolute path`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist invalid (querystring)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["/bar/baz?stu=ff", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["/bar/baz?stu=ff", "/foo"]
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.path_allowlist: path "/bar/baz?stu=ff" is not an absolute path`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist invalid (encoded slash)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["/bar%2fbaz", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["/bar%2fbaz", "/foo"]
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.path_allowlist: path "/bar%2fbaz" is not an absolute path`,
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist ok",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"path_allowlist": ["/bar/baz", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					path_allowlist = ["/bar/baz", "/foo"]
				}
			}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.UIConfig.MetricsProxy.PathAllowlist = []string{"/bar/baz", "/foo"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist defaulted for prometheus",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_provider": "prometheus"
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_provider = "prometheus"
			}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.UIConfig.MetricsProvider = "prometheus"
			rt.UIConfig.MetricsProxy.PathAllowlist = []string{
				"/api/v1/query",
				"/api/v1/query_range",
			}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "metrics_proxy.path_allowlist not overridden with defaults for prometheus",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_provider": "prometheus",
					"metrics_proxy": {
						"path_allowlist": ["/bar/baz", "/foo"]
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_provider = "prometheus"
				metrics_proxy {
					path_allowlist = ["/bar/baz", "/foo"]
				}
			}
			`},
		expected: func(rt *RuntimeConfig) {
			rt.UIConfig.MetricsProvider = "prometheus"
			rt.UIConfig.MetricsProxy.PathAllowlist = []string{"/bar/baz", "/foo"}
			rt.DataDir = dataDir
		},
	})
	run(t, testCase{
		desc: "metrics_proxy.base_url http(s)",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"metrics_proxy": {
						"base_url": "localhost:1234"
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				metrics_proxy {
					base_url = "localhost:1234"
				}
			}
			`},
		expectedErr: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`,
	})
	run(t, testCase{
		desc: "dashboard_url_templates key format",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"dashboard_url_templates": {
						"(*&ASDOUISD)": "localhost:1234"
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				dashboard_url_templates {
					"(*&ASDOUISD)" = "localhost:1234"
				}
			}
			`},
		expectedErr: `ui_config.dashboard_url_templates key names can only contain lowercase alphanumeric, - or _ characters.`,
	})
	run(t, testCase{
		desc: "dashboard_url_templates value format",
		args: []string{`-data-dir=` + dataDir},
		json: []string{`{
				"ui_config": {
					"dashboard_url_templates": {
						"services": "localhost:1234"
					}
				}
			}`},
		hcl: []string{`
			ui_config {
				dashboard_url_templates {
					services = "localhost:1234"
				}
			}
			`},
		expectedErr: `ui_config.dashboard_url_templates values must be a valid http or https URL.`,
	})

	// Per node reconnect timeout test
	run(t, testCase{
		desc: "server and advertised reconnect timeout error",
		args: []string{
			`-data-dir=` + dataDir,
			`-server`,
		},
		hcl: []string{`
				advertise_reconnect_timeout = "5s"
			`},
		json: []string{`
			{
				"advertise_reconnect_timeout": "5s"
			}`},
		expectedErr: "advertise_reconnect_timeout can only be used on a client",
	})

	run(t, testCase{
		desc: "TLS defaults and overrides",
		args: []string{
			`-data-dir=` + dataDir,
		},
		hcl: []string{`
			ports {
				https = 4321
			}

			tls {
				defaults {
					ca_file = "default_ca_file"
					ca_path = "default_ca_path"
					cert_file = "default_cert_file"
					tls_min_version = "TLSv1_2"
					tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
					verify_incoming = true
				}

				internal_rpc {
					ca_file = "internal_rpc_ca_file"
				}

				https {
					cert_file = "https_cert_file"
					tls_min_version = "TLSv1_3"
				}

				grpc {
					verify_incoming = false
					tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"
				}
			}
		`},
		json: []string{`
			{
				"ports": {
					"https": 4321
				},
				"tls": {
					"defaults": {
						"ca_file": "default_ca_file",
						"ca_path": "default_ca_path",
						"cert_file": "default_cert_file",
						"tls_min_version": "TLSv1_2",
						"tls_cipher_suites": "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
						"verify_incoming": true
					},
					"internal_rpc": {
						"ca_file": "internal_rpc_ca_file"
					},
					"https": {
						"cert_file": "https_cert_file",
						"tls_min_version": "TLSv1_3"
					},
					"grpc": {
						"verify_incoming": false,
						"tls_cipher_suites": "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"
					}
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir

			rt.HTTPSPort = 4321
			rt.HTTPSAddrs = []net.Addr{tcpAddr("127.0.0.1:4321")}

			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"

			rt.TLS.InternalRPC.CAFile = "internal_rpc_ca_file"
			rt.TLS.InternalRPC.CAPath = "default_ca_path"
			rt.TLS.InternalRPC.CertFile = "default_cert_file"
			rt.TLS.InternalRPC.TLSMinVersion = "TLSv1_2"
			rt.TLS.InternalRPC.CipherSuites = []types.TLSCipherSuite{types.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256}
			rt.TLS.InternalRPC.VerifyIncoming = true

			rt.TLS.HTTPS.CAFile = "default_ca_file"
			rt.TLS.HTTPS.CAPath = "default_ca_path"
			rt.TLS.HTTPS.CertFile = "https_cert_file"
			rt.TLS.HTTPS.TLSMinVersion = "TLSv1_3"
			rt.TLS.HTTPS.VerifyIncoming = true

			rt.TLS.GRPC.CAFile = "default_ca_file"
			rt.TLS.GRPC.CAPath = "default_ca_path"
			rt.TLS.GRPC.CertFile = "default_cert_file"
			rt.TLS.GRPC.TLSMinVersion = "TLSv1_2"
			rt.TLS.GRPC.CipherSuites = []types.TLSCipherSuite{types.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA}
			rt.TLS.GRPC.VerifyIncoming = false
		},
	})
	run(t, testCase{
		desc: "tls.internal_rpc.verify_server_hostname implies tls.internal_rpc.verify_outgoing",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {
					"internal_rpc": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		hcl: []string{`
			tls {
				internal_rpc {
					verify_server_hostname = true
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir

			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"

			rt.TLS.InternalRPC.VerifyServerHostname = true
			rt.TLS.InternalRPC.VerifyOutgoing = true
		},
	})
	run(t, testCase{
		desc: "tls.defaults.verify_server_hostname implies tls.internal_rpc.verify_outgoing",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {
					"defaults": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		hcl: []string{`
			tls {
				defaults {
					verify_server_hostname = true
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir

			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"

			rt.TLS.InternalRPC.VerifyServerHostname = true
			rt.TLS.InternalRPC.VerifyOutgoing = true
		},
	})
	run(t, testCase{
		desc: "tls.internal_rpc.verify_server_hostname overwrites tls.defaults.verify_server_hostname",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {
					"defaults": {
						"verify_server_hostname": false
					},
					"internal_rpc": {
						"verify_server_hostname": true
					}
				}
			}
		`},
		hcl: []string{`
			tls {
				defaults {
					verify_server_hostname = false
				},
				internal_rpc {
					verify_server_hostname = true
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir

			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"

			rt.TLS.InternalRPC.VerifyServerHostname = true
			rt.TLS.InternalRPC.VerifyOutgoing = true
		},
	})
	run(t, testCase{
		desc: "tls.grpc.use_auto_cert defaults to false",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {
					"grpc": {}
				}
			}
		`},
		hcl: []string{`
			tls {
				grpc {}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"
			rt.TLS.GRPC.UseAutoCert = false
		},
	})
	run(t, testCase{
		desc: "tls.grpc.use_auto_cert defaults to false (II)",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {}
			}
		`},
		hcl: []string{`
			tls {
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"
			rt.TLS.GRPC.UseAutoCert = false
		},
	})
	run(t, testCase{
		desc: "tls.grpc.use_auto_cert defaults to false (III)",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
			}
		`},
		hcl: []string{`
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"
			rt.TLS.GRPC.UseAutoCert = false
		},
	})
	run(t, testCase{
		desc: "tls.grpc.use_auto_cert enabled when true",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {
					"grpc": {
						"use_auto_cert": true
					}
				}
			}
		`},
		hcl: []string{`
			tls {
				grpc {
					use_auto_cert = true
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"
			rt.TLS.GRPC.UseAutoCert = true
		},
	})
	run(t, testCase{
		desc: "tls.grpc.use_auto_cert disabled when false",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"tls": {
					"grpc": {
						"use_auto_cert": false
					}
				}
			}
		`},
		hcl: []string{`
			tls {
				grpc {
					use_auto_cert = false
				}
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.TLS.Domain = "consul."
			rt.TLS.NodeName = "thehostname"
			rt.TLS.GRPC.UseAutoCert = false
		},
	})
	run(t, testCase{
		desc: "logstore defaults",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{``},
		hcl:  []string{``},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.RaftLogStoreConfig.Backend = consul.LogStoreBackendDefault
			rt.RaftLogStoreConfig.WAL.SegmentSize = 64 * 1024 * 1024
		},
	})
	run(t, testCase{
		// this was a bug in the initial config commit. Specifying part of this
		// stanza should still result in sensible defaults for the other parts.
		desc: "wal defaults",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`{
			"raft_logstore": {
				"backend": "boltdb"
			}
		}`},
		hcl: []string{`
			raft_logstore {
				backend = "boltdb"
			}
		`},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.RaftLogStoreConfig.Backend = consul.LogStoreBackendBoltDB
			rt.RaftLogStoreConfig.WAL.SegmentSize = 64 * 1024 * 1024
		},
	})
	run(t, testCase{
		desc: "wal segment size lower bound",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"server": true,
				"raft_logstore": {
					"wal":{
						"segment_size_mb": 0
					}
				}
			}`},
		hcl: []string{`
			server = true
			raft_logstore {
				wal {
					segment_size_mb = 0
				}
			}`},
		expectedErr: "raft_logstore.wal.segment_size_mb cannot be less than",
	})
	run(t, testCase{
		desc: "wal segment size upper bound",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"server": true,
				"raft_logstore": {
					"wal":{
						"segment_size_mb": 1025
					}
				}
			}`},
		hcl: []string{`
			server = true
			raft_logstore {
				wal {
					segment_size_mb = 1025
				}
			}`},
		expectedErr: "raft_logstore.wal.segment_size_mb cannot be greater than",
	})
	run(t, testCase{
		desc: "valid logstore backend",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{`
			{
				"server": true,
				"raft_logstore": {
					"backend": "thecloud"
				}
			}`},
		hcl: []string{`
			server = true
			raft_logstore {
				backend = "thecloud"
			}`},
		expectedErr: "raft_logstore.backend must be one of 'boltdb' or 'wal'",
	})
	run(t, testCase{
		desc: "raft_logstore merging",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			// File 1 has logstore info
			`{
				"raft_logstore": {
					"backend": "wal"
				}
			}`,
			// File 2 doesn't have anything for logstore
			`{
				"enable_debug": true
			}`,
		},
		hcl: []string{
			// File 1 has logstore info
			`
			raft_logstore {
				backend = "wal"
			}`,
			// File 2 doesn't have anything for logstore
			`
			enable_debug = true
			`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			// The logstore settings from first file should not be overridden by a
			// later file with nothing to say about logstores!
			rt.RaftLogStoreConfig.Backend = consul.LogStoreBackendWAL
			rt.EnableDebug = true
		},
	})
	// Multi Port Test Cases
	run(t, testCase{
		desc: "multi_port with ports",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`
			{
				"service": [
					{
					"name": "test1",
					"id": "test1",
					"ports": [
						{
							"name": "http",
							"port": 8080,
							"default": false
						},
						{
							"name": "metrics",
							"port": 9090,
							"default": true
						}
					]
					}
				]
			}
			`,
		},
		hcl: []string{
			`
			service {
				name = "test1"
				id = "test1"
				ports = [
					{
						name = "http"
						port = 8080
						default = false
					},
					{
						name = "metrics"
						port = 9090
						default = true
					}
				]
			}
			`,
		},
		expected: func(rt *RuntimeConfig) {
			rt.DataDir = dataDir
			rt.Services = []*structs.ServiceDefinition{
				{
					Name: "test1",
					ID:   "test1",
					Ports: structs.ServicePorts{
						{
							Name:    "http",
							Port:    8080,
							Default: false,
						},
						{
							Name:    "metrics",
							Port:    9090,
							Default: true,
						},
					},
					Weights: &structs.Weights{Passing: 1, Warning: 1},
				},
			}
		},
	})
	run(t, testCase{
		desc: "multi_port without default should error",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`
			{
				"service": [
					{
					"name": "test1",
					"id": "test1",
					"port": 8080,
					"ports": [
						{
							"name": "http",
							"port": 8080,
							"default": false
						},
						{
							"name": "metrics",
							"port": 9090,
							"default": false
						}
					]
					}
				]
			}
			`,
		},
		hcl: []string{
			`
			service {
				name = "test1"
				id = "test1"
				port = 8080
				ports = [
					{
						name = "http"
						port = 8080
						default = false
					},
					{
						name = "metrics"
						port = 9090
						default = false
					}
				]
			}
			`,
		},
		expectedErr: "One of the Ports must be marked as Default",
	})
	run(t, testCase{
		desc: "multi_port port names must be unique",
		args: []string{
			`-data-dir=` + dataDir,
		},
		json: []string{
			`
			{
				"service": [
					{
					"name": "test1",
					"id": "test1",
					"ports": [
						{
							"name": "http",
							"port": 8080,
							"default": false
						},
						{
							"name": "http",
							"port": 9090,
							"default": true
						}
					]
					}
				]
			}
			`,
		},
		hcl: []string{
			`
			service {
				name = "test1"
				id = "test1"
				ports = [
					{
						name = "http"
						port = 8080
						default = false
					},
					{
						name = "http"
						port = 9090
						default = true
					}
				]
			}
			`,
		},
		expectedErr: "Ports.Name \"http\" has to be unique",
	})
}

func (tc testCase) run(format string, dataDir string) func(t *testing.T) {
	return func(t *testing.T) {
		// clean data dir before every test
		os.RemoveAll(dataDir)
		os.MkdirAll(dataDir, 0755)

		if tc.setup != nil {
			tc.setup()
		}

		opts := tc.opts

		fs := flag.NewFlagSet("", flag.ContinueOnError)
		AddFlags(fs, &opts)
		require.NoError(t, fs.Parse(tc.args))
		require.Len(t, fs.Args(), 0)

		for i, data := range tc.source(format) {
			opts.sources = append(opts.sources, FileSource{
				Name:   fmt.Sprintf("src-%d.%s", i, format),
				Format: format,
				Data:   data,
			})
		}

		patchLoadOptsShims(&opts)
		result, err := Load(opts)
		switch {
		case err == nil && tc.expectedErr != "":
			t.Fatalf("got nil want error to contain %q", tc.expectedErr)
		case err != nil && tc.expectedErr == "":
			t.Fatalf("got error %s want nil", err)
		case err != nil && tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr):
			t.Fatalf("error %q does not contain %q", err.Error(), tc.expectedErr)
		}
		if tc.expectedErr != "" {
			return
		}
		require.Equal(t, tc.expectedWarnings, result.Warnings, "warnings")

		// build a default configuration, then patch the fields we expect to change
		// and compare it with the generated configuration. Since the expected
		// runtime config has been validated we do not need to validate it again.
		expectedOpts := LoadOpts{}
		patchLoadOptsShims(&expectedOpts)
		x, err := newBuilder(expectedOpts)
		require.NoError(t, err)

		expected, err := x.build()
		require.NoError(t, err)
		if tc.expected != nil {
			tc.expected(&expected)
		}

		actual := *result.RuntimeConfig
		// both DataDir fields should always be the same, so test for the
		// invariant, and than updated the expected, so that every test
		// case does not need to set this field.
		require.Equal(t, actual.DataDir, actual.ACLTokens.DataDir)
		expected.ACLTokens.DataDir = actual.ACLTokens.DataDir
		// These fields are always the same
		expected.ACLResolverSettings.Datacenter = expected.Datacenter
		expected.ACLResolverSettings.ACLsEnabled = expected.ACLsEnabled
		expected.ACLResolverSettings.NodeName = expected.NodeName
		expected.ACLResolverSettings.EnterpriseMeta = *structs.NodeEnterpriseMetaInPartition(expected.PartitionOrDefault())

		for i, e := range expected.ConfigEntryBootstrap {
			e.SetHash(actual.ConfigEntryBootstrap[i].GetHash())
		}
		prototest.AssertDeepEqual(t, expected, actual, cmpopts.EquateEmpty())
		if tc.cleanup != nil {
			tc.cleanup()
		}
	}
}

func runCase(t *testing.T, name string, fn func(t *testing.T)) {
	t.Helper()
	t.Run(name, func(t *testing.T) {
		t.Helper()
		t.Log("case:", name)
		fn(t)
	})
}

func TestLoad_InvalidConfigFormat(t *testing.T) {
	_, err := Load(LoadOpts{ConfigFormat: "yaml"})
	require.Error(t, err)
	require.Contains(t, err.Error(), "-config-format must be either 'hcl' or 'json'")
}

// TestFullConfig tests the conversion from a fully populated JSON or
// HCL config file to a RuntimeConfig structure. All fields must be set
// to a unique non-zero value.
func TestLoad_FullConfig(t *testing.T) {
	dataDir := testutil.TempDir(t, "consul")

	cidr := func(s string) *net.IPNet {
		_, n, _ := net.ParseCIDR(s)
		return n
	}

	defaultEntMeta := structs.DefaultEnterpriseMetaInDefaultPartition()
	nodeEntMeta := structs.NodeEnterpriseMetaInDefaultPartition()
	expected := &RuntimeConfig{
		// non-user configurable values
		AEInterval:                     time.Minute,
		CheckDeregisterIntervalMin:     time.Minute,
		CheckReapInterval:              30 * time.Second,
		SegmentNameLimit:               64,
		SyncCoordinateIntervalMin:      15 * time.Second,
		SyncCoordinateRateTarget:       64,
		LocalProxyConfigResyncInterval: 30 * time.Second,

		Revision:          "JNtPSav3",
		Version:           "R909Hblt",
		VersionPrerelease: "ZT1JOQLn",
		VersionMetadata:   "GtTCa13",
		BuildDate:         time.Date(2019, 11, 20, 5, 0, 0, 0, time.UTC),

		// consul configuration
		ConsulCoordinateUpdateBatchSize:  128,
		ConsulCoordinateUpdateMaxBatches: 5,
		ConsulCoordinateUpdatePeriod:     5 * time.Second,
		ConsulRaftElectionTimeout:        5 * time.Second,
		ConsulRaftHeartbeatTimeout:       5 * time.Second,
		ConsulRaftLeaderLeaseTimeout:     2500 * time.Millisecond,
		GossipLANGossipInterval:          25252 * time.Second,
		GossipLANGossipNodes:             6,
		GossipLANProbeInterval:           101 * time.Millisecond,
		GossipLANProbeTimeout:            102 * time.Millisecond,
		GossipLANSuspicionMult:           1235,
		GossipLANRetransmitMult:          1234,
		GossipWANGossipInterval:          6966 * time.Second,
		GossipWANGossipNodes:             2,
		GossipWANProbeInterval:           103 * time.Millisecond,
		GossipWANProbeTimeout:            104 * time.Millisecond,
		GossipWANSuspicionMult:           16385,
		GossipWANRetransmitMult:          16384,
		ConsulServerHealthInterval:       2 * time.Second,

		// user configurable values

		ACLTokens: token.Config{
			EnablePersistence:     true,
			DataDir:               dataDir,
			ACLDefaultToken:       "418fdff1",
			ACLAgentToken:         "bed2377c",
			ACLAgentRecoveryToken: "1dba6aba",
			ACLReplicationToken:   "5795983a",
		},

		ACLsEnabled:       true,
		PrimaryDatacenter: "ejtmd43d",
		ACLResolverSettings: consul.ACLResolverSettings{
			ACLsEnabled:      true,
			Datacenter:       "rzo029wg",
			NodeName:         "otlLxGaI",
			EnterpriseMeta:   *nodeEntMeta,
			ACLDefaultPolicy: "72c2e7a0",
			ACLDownPolicy:    "03eb2aee",
			ACLTokenTTL:      3321 * time.Second,
			ACLPolicyTTL:     1123 * time.Second,
			ACLRoleTTL:       9876 * time.Second,
		},
		ACLEnableKeyListPolicy:           true,
		ACLInitialManagementToken:        "3820e09a",
		ACLTokenReplication:              true,
		AdvertiseAddrLAN:                 ipAddr("17.99.29.16"),
		AdvertiseAddrWAN:                 ipAddr("78.63.37.19"),
		AdvertiseReconnectTimeout:        0 * time.Second,
		AutopilotCleanupDeadServers:      true,
		AutopilotDisableUpgradeMigration: true,
		AutopilotLastContactThreshold:    12705 * time.Second,
		AutopilotMaxTrailingLogs:         17849,
		AutopilotMinQuorum:               3,
		AutopilotRedundancyZoneTag:       "3IsufDJf",
		AutopilotServerStabilizationTime: 23057 * time.Second,
		AutopilotUpgradeVersionTag:       "W9pDwFAL",
		BindAddr:                         ipAddr("16.99.34.17"),
		BootstrapExpect:                  53,
		Cache: cache.Options{
			EntryFetchMaxBurst: 42,
			EntryFetchRate:     0.334,
		},
		CheckOutputMaxSize: checks.DefaultBufSize,
		Checks: []*structs.CheckDefinition{
			{
				ID:         "uAjE6m9Z",
				Name:       "QsZRGpYr",
				Notes:      "VJ7Sk4BY",
				ServiceID:  "lSulPcyz",
				Token:      "toO59sh8",
				Status:     "9RlWsXMV",
				ScriptArgs: []string{"4BAJttck", "4D2NPtTQ"},
				HTTP:       "dohLcyQ2",
				Header: map[string][]string{
					"ZBfTin3L": {"1sDbEqYG", "lJGASsWK"},
					"Ui0nU99X": {"LMccm3Qe", "k5H5RggQ"},
				},
				Method:                         "aldrIQ4l",
				Body:                           "wSjTy7dg",
				DisableRedirects:               true,
				TCP:                            "RJQND605",
				TCPUseTLS:                      false,
				H2PING:                         "9N1cSb5B",
				H2PingUseTLS:                   false,
				OSService:                      "aAjE6m9Z",
				Interval:                       22164 * time.Second,
				OutputMaxSize:                  checks.DefaultBufSize,
				DockerContainerID:              "ipgdFtjd",
				Shell:                          "qAeOYy0M",
				TLSServerName:                  "bdeb5f6a",
				TLSSkipVerify:                  true,
				Timeout:                        1813 * time.Second,
				DeregisterCriticalServiceAfter: 14232 * time.Second,
			},
			{
				ID:         "Cqq95BhP",
				Name:       "3qXpkS0i",
				Notes:      "sb5qLTex",
				ServiceID:  "CmUUcRna",
				Token:      "a3nQzHuy",
				Status:     "irj26nf3",
				ScriptArgs: []string{"9s526ogY", "gSlOHj1w"},
				HTTP:       "yzhgsQ7Y",
				Header: map[string][]string{
					"zcqwA8dO": {"qb1zx0DL", "sXCxPFsD"},
					"qxvdnSE9": {"6wBPUYdF", "YYh8wtSZ"},
				},
				Method:                         "gLrztrNw",
				Body:                           "0jkKgGUC",
				DisableRedirects:               false,
				OutputMaxSize:                  checks.DefaultBufSize,
				TCP:                            "4jG5casb",
				TCPUseTLS:                      false,
				H2PING:                         "HCHU7gEb",
				H2PingUseTLS:                   false,
				OSService:                      "aqq95BhP",
				Interval:                       28767 * time.Second,
				DockerContainerID:              "THW6u7rL",
				Shell:                          "C1Zt3Zwh",
				TLSServerName:                  "6adc3bfb",
				TLSSkipVerify:                  true,
				Timeout:                        18506 * time.Second,
				DeregisterCriticalServiceAfter: 2366 * time.Second,
			},
			{
				ID:         "fZaCAXww",
				Name:       "OOM2eo0f",
				Notes:      "zXzXI9Gt",
				ServiceID:  "L8G0QNmR",
				Token:      "oo4BCTgJ",
				Status:     "qLykAl5u",
				ScriptArgs: []string{"f3BemRjy", "e5zgpef7"},
				HTTP:       "29B93haH",
				Header: map[string][]string{
					"hBq0zn1q": {"2a9o9ZKP", "vKwA5lR6"},
					"f3r6xFtM": {"RyuIdDWv", "QbxEcIUM"},
				},
				Method:                         "Dou0nGT5",
				Body:                           "5PBQd2OT",
				DisableRedirects:               true,
				OutputMaxSize:                  checks.DefaultBufSize,
				TCP:                            "JY6fTTcw",
				TCPUseTLS:                      false,
				H2PING:                         "rQ8eyCSF",
				H2PingUseTLS:                   false,
				OSService:                      "aZaCAXww",
				Interval:                       18714 * time.Second,
				DockerContainerID:              "qF66POS9",
				Shell:                          "sOnDy228",
				TLSServerName:                  "7BdnzBYk",
				TLSSkipVerify:                  true,
				Timeout:                        5954 * time.Second,
				DeregisterCriticalServiceAfter: 13209 * time.Second,
			},
		},
		CheckUpdateInterval: 16507 * time.Second,
		ClientAddrs:         []*net.IPAddr{ipAddr("93.83.18.19")},
		ConfigEntryBootstrap: []structs.ConfigEntry{
			&structs.ProxyConfigEntry{
				Kind:           structs.ProxyDefaults,
				Name:           structs.ProxyConfigGlobal,
				EnterpriseMeta: *defaultEntMeta,
				Config: map[string]interface{}{
					"foo": "bar",
					// has to be a float due to being a map[string]interface
					"bar": float64(1),
				},
			},
		},
		AutoEncryptTLS:      false,
		AutoEncryptDNSSAN:   []string{"a.com", "b.com"},
		AutoEncryptIPSAN:    []net.IP{net.ParseIP("192.168.4.139"), net.ParseIP("192.168.4.140")},
		AutoEncryptAllowTLS: true,
		AutoConfig: AutoConfig{
			Enabled:         false,
			IntroToken:      "OpBPGRwt",
			IntroTokenFile:  "gFvAXwI8",
			DNSSANs:         []string{"6zdaWg9J"},
			IPSANs:          []net.IP{net.IPv4(198, 18, 99, 99)},
			ServerAddresses: []string{"198.18.100.1"},
			Authorizer: AutoConfigAuthorizer{
				Enabled:         true,
				AllowReuse:      true,
				ClaimAssertions: []string{"value.node == \"${node}\""},
				AuthMethod: structs.ACLAuthMethod{
					Name:           "Auto Config Authorizer",
					Type:           "jwt",
					EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
					Config: map[string]interface{}{
						"JWTValidationPubKeys": []string{"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"},
						"ClaimMappings": map[string]string{
							"node": "node",
						},
						"BoundIssuer":    "consul",
						"BoundAudiences": []string{"consul-cluster-1"},
						"ListClaimMappings": map[string]string{
							"foo": "bar",
						},
						"OIDCDiscoveryURL":    "",
						"OIDCDiscoveryCACert": "",
						"JWKSURL":             "",
						"JWKSCACert":          "",
						"ExpirationLeeway":    0 * time.Second,
						"NotBeforeLeeway":     0 * time.Second,
						"ClockSkewLeeway":     0 * time.Second,
						"JWTSupportedAlgs":    []string(nil),
					},
				},
			},
		},
		ConnectEnabled:        true,
		ConnectSidecarMinPort: 8888,
		ConnectSidecarMaxPort: 9999,
		ExposeMinPort:         1111,
		ExposeMaxPort:         2222,
		ConnectCAProvider:     "consul",
		ConnectCAConfig: map[string]interface{}{
			"IntermediateCertTTL": "8760h",
			"LeafCertTTL":         "1h",
			"RootCertTTL":         "96360h",
			"CSRMaxPerSecond":     float64(100),
			"CSRMaxConcurrent":    float64(2),
		},
		ConnectVirtualIPCIDRv4:                 "240.0.0.0/4",
		ConnectVirtualIPCIDRv6:                 "2000::/3",
		ConnectMeshGatewayWANFederationEnabled: false,
		Cloud: hcpconfig.CloudConfig{
			ResourceID:   "N43DsscE",
			ClientID:     "6WvsDZCP",
			ClientSecret: "lCSMHOpB",
			Hostname:     "DH4bh7aC",
			AuthURL:      "332nCdR2",
			ScadaAddress: "aoeusth232",
			NodeID:       types.NodeID("AsUIlw99"),
			NodeName:     "otlLxGaI",
		},
		DNSAddrs:                         []net.Addr{tcpAddr("93.95.95.81:7001"), udpAddr("93.95.95.81:7001")},
		DNSARecordLimit:                  29907,
		DNSAllowStale:                    true,
		DNSDisableCompression:            true,
		DNSDomain:                        "7W1xXSqd",
		DNSAltDomain:                     "1789hsd",
		DNSEnableTruncate:                true,
		DNSMaxStale:                      29685 * time.Second,
		DNSNodeTTL:                       7084 * time.Second,
		DNSOnlyPassing:                   true,
		DNSPort:                          7001,
		DNSRecursorStrategy:              "sequential",
		DNSRecursorTimeout:               4427 * time.Second,
		DNSRecursors:                     []string{"63.38.39.58", "92.49.18.18"},
		DNSSOA:                           RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0},
		DNSServiceTTL:                    map[string]time.Duration{"*": 32030 * time.Second},
		DNSUDPAnswerLimit:                29909,
		DNSNodeMetaTXT:                   true,
		DNSUseCache:                      true,
		DNSCacheMaxAge:                   5 * time.Minute,
		DataDir:                          dataDir,
		Datacenter:                       "rzo029wg",
		DefaultQueryTime:                 16743 * time.Second,
		DisableAnonymousSignature:        true,
		DisableCoordinates:               true,
		DisableHostNodeID:                true,
		DisableHTTPUnprintableCharFilter: true,
		DisableKeyringFile:               true,
		DisableRemoteExec:                true,
		DisableUpdateCheck:               true,
		DiscardCheckOutput:               true,
		DiscoveryMaxStale:                5 * time.Second,
		EnableAgentTLSForChecks:          true,
		EnableCentralServiceConfig:       false,
		EnableDebug:                      true,
		DisableKVKeyValidation:           false,
		EnableRemoteScriptChecks:         true,
		EnableLocalScriptChecks:          true,
		EncryptKey:                       "A4wELWqH",
		Experiments:                      []string{"foo"},
		StaticRuntimeConfig: StaticRuntimeConfig{
			EncryptVerifyIncoming: true,
			EncryptVerifyOutgoing: true,
		},

		GRPCPort:              4881,
		GRPCAddrs:             []net.Addr{tcpAddr("32.31.61.91:4881")},
		GRPCTLSPort:           5201,
		GRPCTLSAddrs:          []net.Addr{tcpAddr("23.14.88.19:5201")},
		GRPCKeepaliveInterval: 33 * time.Second,
		GRPCKeepaliveTimeout:  22 * time.Second,
		HTTPAddrs:             []net.Addr{tcpAddr("83.39.91.39:7999")},
		HTTPBlockEndpoints:    []string{"RBvAFcGD", "fWOWFznh"},
		AllowWriteHTTPFrom:    []*net.IPNet{cidr("127.0.0.0/8"), cidr("22.33.44.55/32"), cidr("0.0.0.0/0")},
		HTTPPort:              7999,
		HTTPResponseHeaders:   map[string]string{"M6TKa9NP": "xjuxjOzQ", "JRCrHZed": "rl0mTx81"},
		HTTPSAddrs:            []net.Addr{tcpAddr("95.17.17.19:15127")},
		HTTPMaxConnsPerClient: 100,
		HTTPMaxHeaderBytes:    10,
		HTTPSHandshakeTimeout: 2391 * time.Millisecond,
		HTTPSPort:             15127,
		HTTPUseCache:          false,
		KVMaxValueSize:        1234567800,
		LeaveDrainTime:        8265 * time.Second,
		LeaveOnTerm:           true,
		Locality: &Locality{
			Region: strPtr("us-east-2"),
			Zone:   strPtr("us-east-2b"),
		},
		Logging: logging.Config{
			LogLevel:       "k1zo9Spt",
			LogJSON:        true,
			EnableSyslog:   true,
			SyslogFacility: "hHv79Uia",
		},
		MaxQueryTime:            18237 * time.Second,
		NodeID:                  types.NodeID("AsUIlw99"),
		NodeMeta:                map[string]string{"5mgGQMBk": "mJLtVMSG", "A7ynFMJB": "0Nx6RGab"},
		NodeName:                "otlLxGaI",
		ReadReplica:             true,
		PeeringEnabled:          true,
		PidFile:                 "43xN80Km",
		PrimaryGateways:         []string{"aej8eeZo", "roh2KahS"},
		PrimaryGatewaysInterval: 18866 * time.Second,
		RPCAdvertiseAddr:        tcpAddr("17.99.29.16:3757"),
		RPCBindAddr:             tcpAddr("16.99.34.17:3757"),
		RPCHandshakeTimeout:     1932 * time.Millisecond,
		RPCClientTimeout:        62 * time.Second,
		RPCHoldTimeout:          15707 * time.Second,
		RPCProtocol:             30793,
		RPCRateLimit:            12029.43,
		RPCMaxBurst:             44848,
		RPCMaxConnsPerClient:    2954,
		RaftProtocol:            3,
		RaftSnapshotThreshold:   16384,
		RaftSnapshotInterval:    30 * time.Second,
		RaftTrailingLogs:        83749,
		RaftPreVoteDisabled:     false,
		ReconnectTimeoutLAN:     23739 * time.Second,
		ReconnectTimeoutWAN:     26694 * time.Second,
		RequestLimitsMode:       consulrate.ModePermissive,
		RequestLimitsReadRate:   99.0,
		RequestLimitsWriteRate:  101.0,
		RejoinAfterLeave:        true,
		RetryJoinIntervalLAN:    8067 * time.Second,
		RetryJoinIntervalWAN:    28866 * time.Second,
		RetryJoinLAN:            []string{"pbsSFY7U", "l0qLtWij", "LR3hGDoG", "MwVpZ4Up"},
		RetryJoinMaxAttemptsLAN: 913,
		RetryJoinMaxAttemptsWAN: 23160,
		RetryJoinWAN:            []string{"PFsR02Ye", "rJdQIhER", "EbFSc3nA", "kwXTh623"},
		RPCConfig:               consul.RPCConfig{EnableStreaming: true},
		SegmentLimit:            123,
		SerfPortLAN:             8301,
		SerfPortWAN:             8302,
		ServerMode:              true,
		ServerName:              "Oerr9n1G",
		ServerRejoinAgeMax:      604800 * time.Second,
		ServerPort:              3757,
		Services: []*structs.ServiceDefinition{
			{
				ID:      "wI1dzxS4",
				Name:    "7IszXMQ1",
				Tags:    []string{"0Zwg8l6v", "zebELdN5"},
				Address: "9RhqPSPB",
				Token:   "myjKJkWH",
				Port:    72219,
				Weights: &structs.Weights{
					Passing: 1,
					Warning: 1,
				},
				EnableTagOverride: true,
				Checks: []*structs.CheckType{
					{
						CheckID:    "qmfeO5if",
						Name:       "atDGP7n5",
						Status:     "pDQKEhWL",
						Notes:      "Yt8EDLev",
						ScriptArgs: []string{"81EDZLPa", "bPY5X8xd"},
						HTTP:       "qzHYvmJO",
						Header: map[string][]string{
							"UkpmZ3a3": {"2dfzXuxZ"},
							"cVFpko4u": {"gGqdEB6k", "9LsRo22u"},
						},
						Method:                         "X5DrovFc",
						Body:                           "WeikigLh",
						DisableRedirects:               true,
						OutputMaxSize:                  checks.DefaultBufSize,
						TCP:                            "ICbxkpSF",
						H2PING:                         "7s7BbMyb",
						H2PingUseTLS:                   false,
						OSService:                      "amfeO5if",
						Interval:                       24392 * time.Second,
						DockerContainerID:              "ZKXr68Yb",
						Shell:                          "CEfzx0Fo",
						TLSServerName:                  "4f191d4F",
						TLSSkipVerify:                  true,
						Timeout:                        38333 * time.Second,
						DeregisterCriticalServiceAfter: 44214 * time.Second,
					},
				},
				// Note that although this SidecarService is only syntax sugar for
				// registering another service, that has to happen in the agent code so
				// it can make intelligent decisions about automatic port assignments
				// etc. So we expect config just to pass it through verbatim.
				Connect: &structs.ServiceConnect{
					SidecarService: &structs.ServiceDefinition{
						Weights: &structs.Weights{
							Passing: 1,
							Warning: 1,
						},
					},
				},
				Locality: &structs.Locality{
					Region: "us-east-1",
					Zone:   "us-east-1a",
				},
			},
			{
				ID:      "MRHVMZuD",
				Name:    "6L6BVfgH",
				Tags:    []string{"7Ale4y6o", "PMBW08hy"},
				Address: "R6H6g8h0",
				Token:   "ZgY8gjMI",
				Port:    38292,
				Weights: &structs.Weights{
					Passing: 1979,
					Warning: 6,
				},
				EnableTagOverride: true,
				Checks: structs.CheckTypes{
					&structs.CheckType{
						CheckID:    "GTti9hCo",
						Name:       "9OOS93ne",
						Notes:      "CQy86DH0",
						Status:     "P0SWDvrk",
						ScriptArgs: []string{"EXvkYIuG", "BATOyt6h"},
						HTTP:       "u97ByEiW",
						Header: map[string][]string{
							"MUlReo8L": {"AUZG7wHG", "gsN0Dc2N"},
							"1UJXjVrT": {"OJgxzTfk", "xZZrFsq7"},
						},
						Method:                         "5wkAxCUE",
						Body:                           "7CRjCJyz",
						OutputMaxSize:                  checks.DefaultBufSize,
						TCP:                            "MN3oA9D2",
						H2PING:                         "OV6Q2XEg",
						H2PingUseTLS:                   false,
						OSService:                      "GTti9hCA",
						Interval:                       32718 * time.Second,
						DockerContainerID:              "cU15LMet",
						Shell:                          "nEz9qz2l",
						TLSServerName:                  "f43ouY7a",
						TLSSkipVerify:                  true,
						Timeout:                        34738 * time.Second,
						DeregisterCriticalServiceAfter: 84282 * time.Second,
					},
					&structs.CheckType{
						CheckID:                        "UHsDeLxG",
						Name:                           "PQSaPWlT",
						Notes:                          "jKChDOdl",
						Status:                         "5qFz6OZn",
						OutputMaxSize:                  checks.DefaultBufSize,
						Timeout:                        4868 * time.Second,
						TTL:                            11222 * time.Second,
						DeregisterCriticalServiceAfter: 68482 * time.Second,
					},
				},
				Connect: &structs.ServiceConnect{},
			},
			{
				ID:   "Kh81CPF6",
				Name: "Kh81CPF6-proxy",
				Port: 31471,
				Kind: "connect-proxy",
				Proxy: &structs.ConnectProxyConfig{
					DestinationServiceName: "6L6BVfgH",
					DestinationServiceID:   "6L6BVfgH-id",
					LocalServiceAddress:    "127.0.0.2",
					LocalServicePort:       23759,
					Config: map[string]interface{}{
						"cedGGtZf": "pWrUNiWw",
					},
					Upstreams: structs.Upstreams{
						{
							DestinationType:      "service", // Default should be explicitly filled
							DestinationName:      "KPtAj2cb",
							DestinationPartition: defaultEntMeta.PartitionOrEmpty(),
							DestinationNamespace: defaultEntMeta.NamespaceOrEmpty(),
							LocalBindPort:        4051,
							Config: map[string]interface{}{
								"kzRnZOyd": "nUNKoL8H",
							},
						},
						{
							DestinationType:      "prepared_query",
							DestinationNamespace: "9nakw0td",
							DestinationPartition: "part-9nakw0td",
							DestinationName:      "KSd8HsRl",
							LocalBindPort:        11884,
							LocalBindAddress:     "127.24.88.0",
						},
						{
							DestinationType:      "prepared_query",
							DestinationNamespace: "9nakw0td",
							DestinationPartition: "part-9nakw0td",
							DestinationName:      "placeholder",
							LocalBindSocketPath:  "/foo/bar/upstream",
							LocalBindSocketMode:  "0600",
						},
					},
					Expose: structs.ExposeConfig{
						Checks: true,
						Paths: []structs.ExposePath{
							{
								Path:          "/health",
								LocalPathPort: 8080,
								ListenerPort:  21500,
								Protocol:      "http",
							},
						},
					},
					Mode: structs.ProxyModeTransparent,
					TransparentProxy: structs.TransparentProxyConfig{
						OutboundListenerPort: 10101,
						DialedDirectly:       true,
					},
				},
				Weights: &structs.Weights{
					Passing: 1,
					Warning: 1,
				},
			},
			{
				ID:   "kvVqbwSE",
				Kind: "mesh-gateway",
				Name: "gw-primary-dc",
				Port: 27147,
				Proxy: &structs.ConnectProxyConfig{
					Config: map[string]interface{}{
						"1CuJHVfw": "Kzqsa7yc",
					},
					Upstreams: structs.Upstreams{},
				},
				Weights: &structs.Weights{
					Passing: 1,
					Warning: 1,
				},
			},
			{
				ID:      "AI1d2xY4",
				Name:    "discovery-multi-port",
				Tags:    []string{"0Zwg8l6v", "zebELdN5"},
				Address: "9RhAPlPB",
				Token:   "myjKJkWH",
				Weights: &structs.Weights{
					Passing: 1,
					Warning: 1,
				},
				Ports: structs.ServicePorts{
					{
						Name:    "http",
						Port:    8080,
						Default: true,
					},
					{
						Name:    "admin",
						Port:    9090,
						Default: false,
					},
				},
				EnableTagOverride: true,
			},
			{
				ID:   "dLOXpSCI",
				Name: "o1ynPkp0",
				TaggedAddresses: map[string]structs.ServiceAddress{
					"lan": {
						Address: "2d79888a",
						Port:    2143,
					},
					"wan": {
						Address: "d4db85e2",
						Port:    6109,
					},
				},
				Tags:    []string{"nkwshvM5", "NTDWn3ek"},
				Address: "cOlSOhbp",
				Token:   "msy7iWER",
				Meta:    map[string]string{"mymeta": "data"},
				Port:    24237,
				Weights: &structs.Weights{
					Passing: 100,
					Warning: 1,
				},
				EnableTagOverride: true,
				Connect: &structs.ServiceConnect{
					Native: true,
				},
				Locality: &structs.Locality{
					Region: "us-west-1",
					Zone:   "us-west-1a",
				},
				Checks: structs.CheckTypes{
					&structs.CheckType{
						CheckID:    "Zv99e9Ka",
						Name:       "sgV4F7Pk",
						Notes:      "yP5nKbW0",
						Status:     "7oLMEyfu",
						ScriptArgs: []string{"5wEZtZpv", "0Ihyk8cS"},
						HTTP:       "KyDjGY9H",
						Header: map[string][]string{
							"gv5qefTz": {"5Olo2pMG", "PvvKWQU5"},
							"SHOVq1Vv": {"jntFhyym", "GYJh32pp"},
						},
						Method:                         "T66MFBfR",
						Body:                           "OwGjTFQi",
						DisableRedirects:               true,
						OutputMaxSize:                  checks.DefaultBufSize,
						TCP:                            "bNnNfx2A",
						H2PING:                         "qC1pidiW",
						H2PingUseTLS:                   false,
						OSService:                      "ZA99e9Ka",
						Interval:                       22224 * time.Second,
						DockerContainerID:              "ipgdFtjd",
						Shell:                          "omVZq7Sz",
						TLSServerName:                  "axw5QPL5",
						TLSSkipVerify:                  true,
						Timeout:                        18913 * time.Second,
						DeregisterCriticalServiceAfter: 8482 * time.Second,
					},
					&structs.CheckType{
						CheckID:    "G79O6Mpr",
						Name:       "IEqrzrsd",
						Notes:      "SVqApqeM",
						Status:     "XXkVoZXt",
						ScriptArgs: []string{"wD05Bvao", "rLYB7kQC"},
						HTTP:       "kyICZsn8",
						Header: map[string][]string{
							"4ebP5vL4": {"G20SrL5Q", "DwPKlMbo"},
							"p2UI34Qz": {"UsG1D0Qh", "NHhRiB6s"},
						},
						Method:                         "ciYHWors",
						Body:                           "lUVLGYU7",
						DisableRedirects:               false,
						OutputMaxSize:                  checks.DefaultBufSize,
						TCP:                            "FfvCwlqH",
						H2PING:                         "spI3muI3",
						H2PingUseTLS:                   false,
						OSService:                      "GAaO6Mpr",
						Interval:                       12356 * time.Second,
						DockerContainerID:              "HBndBU6R",
						Shell:                          "hVI33JjA",
						TLSServerName:                  "7uwWOnUS",
						TLSSkipVerify:                  true,
						Timeout:                        38282 * time.Second,
						DeregisterCriticalServiceAfter: 4992 * time.Second,
					},
					&structs.CheckType{
						CheckID:    "RMi85Dv8",
						Name:       "iehanzuq",
						Status:     "rCvn53TH",
						Notes:      "fti5lfF3",
						ScriptArgs: []string{"16WRUmwS", "QWk7j7ae"},
						HTTP:       "dl3Fgme3",
						Header: map[string][]string{
							"rjm4DEd3": {"2m3m2Fls"},
							"l4HwQ112": {"fk56MNlo", "dhLK56aZ"},
						},
						Method:                         "9afLm3Mj",
						Body:                           "wVVL2V6f",
						DisableRedirects:               true,
						OutputMaxSize:                  checks.DefaultBufSize,
						TCP:                            "fjiLFqVd",
						H2PING:                         "5NbNWhan",
						H2PingUseTLS:                   false,
						OSService:                      "RAa85Dv8",
						Interval:                       23926 * time.Second,
						DockerContainerID:              "dO5TtRHk",
						Shell:                          "e6q2ttES",
						TLSServerName:                  "ECSHk8WF",
						TLSSkipVerify:                  true,
						Timeout:                        38483 * time.Second,
						DeregisterCriticalServiceAfter: 68787 * time.Second,
					},
				},
			},
		},
		UseStreamingBackend:  true,
		SerfAdvertiseAddrLAN: tcpAddr("17.99.29.16:8301"),
		SerfAdvertiseAddrWAN: tcpAddr("78.63.37.19:8302"),
		SerfBindAddrLAN:      tcpAddr("99.43.63.15:8301"),
		SerfBindAddrWAN:      tcpAddr("67.88.33.19:8302"),
		SerfAllowedCIDRsLAN:  []net.IPNet{},
		SerfAllowedCIDRsWAN:  []net.IPNet{},
		SessionTTLMin:        26627 * time.Second,
		SkipLeaveOnInt:       true,
		Telemetry: lib.TelemetryConfig{
			CirconusAPIApp:                     "p4QOTe9j",
			CirconusAPIToken:                   "E3j35V23",
			CirconusAPIURL:                     "mEMjHpGg",
			CirconusBrokerID:                   "BHlxUhed",
			CirconusBrokerSelectTag:            "13xy1gHm",
			CirconusCheckDisplayName:           "DRSlQR6n",
			CirconusCheckForceMetricActivation: "Ua5FGVYf",
			CirconusCheckID:                    "kGorutad",
			CirconusCheckInstanceID:            "rwoOL6R4",
			CirconusCheckSearchTag:             "ovT4hT4f",
			CirconusCheckTags:                  "prvO4uBl",
			CirconusSubmissionInterval:         "DolzaflP",
			CirconusSubmissionURL:              "gTcbS93G",
			DisableHostname:                    true,
			DogstatsdAddr:                      "0wSndumK",
			DogstatsdTags:                      []string{"3N81zSUB", "Xtj8AnXZ"},
			RetryFailedConfiguration:           true,
			FilterDefault:                      true,
			AllowedPrefixes:                    []string{"oJotS8XJ"},
			BlockedPrefixes:                    []string{"cazlEhGn", "ftO6DySn.rpc.server.call"},
			MetricsPrefix:                      "ftO6DySn",
			StatsdAddr:                         "drce87cy",
			StatsiteAddr:                       "HpFwKB8R",
			PrometheusOpts: prometheus.PrometheusOpts{
				Expiration: 15 * time.Second,
				Name:       "ftO6DySn", // notice this is the same as the metrics prefix
			},
			EnableHostMetrics:             true,
			DisablePerTenancyUsageMetrics: true,
		},
		TLS: tlsutil.Config{
			InternalRPC: tlsutil.ProtocolConfig{
				VerifyIncoming:       true,
				CAFile:               "mKl19Utl",
				CAPath:               "lOp1nhPa",
				CertFile:             "dfJ4oPln",
				KeyFile:              "aL1Knkpo",
				TLSMinVersion:        types.TLSv1_1,
				CipherSuites:         []types.TLSCipherSuite{types.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, types.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA},
				VerifyOutgoing:       true,
				VerifyServerHostname: true,
			},
			GRPC: tlsutil.ProtocolConfig{
				VerifyIncoming: true,
				CAFile:         "lOp1nhJk",
				CAPath:         "fLponKpl",
				CertFile:       "a674klPn",
				KeyFile:        "1y4prKjl",
				TLSMinVersion:  types.TLSv1_0,
				CipherSuites:   []types.TLSCipherSuite{types.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, types.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA},
				VerifyOutgoing: false,
				UseAutoCert:    true,
			},
			HTTPS: tlsutil.ProtocolConfig{
				VerifyIncoming: true,
				CAFile:         "7Yu1PolM",
				CAPath:         "nu4PlHzn",
				CertFile:       "1yrhPlMk",
				KeyFile:        "1bHapOkL",
				TLSMinVersion:  types.TLSv1_3,
				VerifyOutgoing: true,
			},
			NodeName:                "otlLxGaI",
			ServerName:              "Oerr9n1G",
			ServerMode:              true,
			Domain:                  "7W1xXSqd",
			EnableAgentTLSForChecks: true,
		},
		TaggedAddresses: map[string]string{
			"7MYgHrYH": "dALJAhLD",
			"h6DdBy6K": "ebrr9zZ8",
			"lan":      "17.99.29.16",
			"lan_ipv4": "17.99.29.16",
			"wan":      "78.63.37.19",
			"wan_ipv4": "78.63.37.19",
		},
		TranslateWANAddrs: true,
		TxnMaxReqLen:      567800000,
		UIConfig: UIConfig{
			Dir:                        "pVncV4Ey",
			ContentPath:                "/qp1WRhYH/", // slashes are added in parsing
			MetricsProvider:            "sgnaoa_lower_case",
			MetricsProviderFiles:       []string{"sgnaMFoa", "dicnwkTH"},
			MetricsProviderOptionsJSON: "{\"DIbVQadX\": 1}",
			MetricsProxy: UIMetricsProxy{
				BaseURL: "http://foo.bar",
				AddHeaders: []UIMetricsProxyAddHeader{
					{
						Name:  "p3nynwc9",
						Value: "TYBgnN2F",
					},
				},
				PathAllowlist: []string{"/aSh3cu", "/eiK/2Th"},
			},
			DashboardURLTemplates: map[string]string{"u2eziu2n_lower_case": "http://lkjasd.otr"},
		},
		UnixSocketUser:  "E0nB1DwA",
		UnixSocketGroup: "8pFodrV8",
		UnixSocketMode:  "E8sAwOv4",
		Watches: []map[string]interface{}{
			{
				"type":       "key",
				"datacenter": "GyE6jpeW",
				"key":        "j9lF1Tve",
				"handler":    "90N7S4LN",
			},
			{
				"type":       "keyprefix",
				"datacenter": "fYrl3F5d",
				"key":        "sl3Dffu7",
				"args":       []interface{}{"dltjDJ2a", "flEa7C2d"},
			},
		},
		XDSUpdateRateLimit: 9526.2,
		RaftLogStoreConfig: consul.RaftLogStoreConfig{
			Backend:         consul.LogStoreBackendWAL,
			DisableLogCache: true,
			Verification: consul.RaftLogStoreVerificationConfig{
				Enabled:  true,
				Interval: 12345 * time.Second,
			},
			BoltDB: consul.RaftBoltDBConfig{NoFreelistSync: true},
			WAL:    consul.WALConfig{SegmentSize: 15 * 1024 * 1024},
		},
		AutoReloadConfigCoalesceInterval: 1 * time.Second,
		EnableXDSLoadBalancing:           false,
	}
	entFullRuntimeConfig(expected)

	expectedWarns := []string{
		deprecationWarning("acl_datacenter", "primary_datacenter"),
		deprecationWarning("acl_agent_master_token", "acl.tokens.agent_recovery"),
		deprecationWarning("acl.tokens.agent_master", "acl.tokens.agent_recovery"),
		deprecationWarning("acl_agent_token", "acl.tokens.agent"),
		deprecationWarning("acl_token", "acl.tokens.default"),
		deprecationWarning("acl_master_token", "acl.tokens.initial_management"),
		deprecationWarning("acl.tokens.master", "acl.tokens.initial_management"),
		deprecationWarning("acl_replication_token", "acl.tokens.replication"),
		deprecationWarning("enable_acl_replication", "acl.enable_token_replication"),
		deprecationWarning("acl_default_policy", "acl.default_policy"),
		deprecationWarning("acl_down_policy", "acl.down_policy"),
		deprecationWarning("acl_ttl", "acl.token_ttl"),
		deprecationWarning("acl_enable_key_list_policy", "acl.enable_key_list_policy"),
		`bootstrap_expect > 0: expecting 53 servers`,
		deprecationWarning("ca_file", "tls.defaults.ca_file"),
		deprecationWarning("ca_path", "tls.defaults.ca_path"),
		deprecationWarning("cert_file", "tls.defaults.cert_file"),
		deprecationWarning("key_file", "tls.defaults.key_file"),
		deprecationWarning("tls_cipher_suites", "tls.defaults.tls_cipher_suites"),
		deprecationWarning("tls_min_version", "tls.defaults.tls_min_version"),
		deprecationWarning("verify_incoming", "tls.defaults.verify_incoming"),
		deprecationWarning("verify_incoming_https", "tls.https.verify_incoming"),
		deprecationWarning("verify_incoming_rpc", "tls.internal_rpc.verify_incoming"),
		deprecationWarning("verify_outgoing", "tls.defaults.verify_outgoing"),
		deprecationWarning("verify_server_hostname", "tls.internal_rpc.verify_server_hostname"),
		"The 'tls_prefer_server_cipher_suites' field is deprecated and will be ignored.",
		deprecationWarning("start_join", "retry_join"),
		deprecationWarning("start_join_wan", "retry_join_wan"),
	}
	expectedWarns = append(expectedWarns, enterpriseConfigKeyWarnings...)

	// FIXME: ensure that all fields are set to unique non-zero values.
	// There are many fields that are not set to non-zero values, so this nonZero
	// check does not actually work.
	if err := nonZero("RuntimeConfig", nil, expected); err != nil {
		t.Log(err)
	}

	for _, format := range []string{"json", "hcl"} {
		t.Run(format, func(t *testing.T) {
			opts := LoadOpts{
				ConfigFiles: []string{"testdata/full-config." + format},
				HCL:         []string{fmt.Sprintf(`data_dir = "%s"`, dataDir)},
			}
			opts.Overrides = append(opts.Overrides, versionSource("JNtPSav3", "R909Hblt", "ZT1JOQLn", "GtTCa13",
				time.Date(2019, 11, 20, 5, 0, 0, 0, time.UTC)))
			r, err := Load(opts)
			require.NoError(t, err)
			for i, e := range expected.ConfigEntryBootstrap {
				e.SetHash(r.RuntimeConfig.ConfigEntryBootstrap[i].GetHash())
			}
			prototest.AssertDeepEqual(t, expected, r.RuntimeConfig)
			require.ElementsMatch(t, expectedWarns, r.Warnings, "Warnings: %#v", r.Warnings)
		})
	}
}

// nonZero verifies recursively that all fields are set to unique,
// non-zero and non-nil values.
//
// struct: check all fields recursively
// slice: check len > 0 and all values recursively
// ptr: check not nil
// bool: check not zero (cannot check uniqueness)
// string, int, uint: check not zero and unique
// other: error
func nonZero(name string, uniq map[interface{}]string, v interface{}) error {
	if v == nil {
		return fmt.Errorf("%q is nil", name)
	}

	if uniq == nil {
		uniq = map[interface{}]string{}
	}

	isUnique := func(v interface{}) error {
		if other := uniq[v]; other != "" {
			return fmt.Errorf("%q and %q both use value %q", name, other, v)
		}
		uniq[v] = name
		return nil
	}

	val, typ := reflect.ValueOf(v), reflect.TypeOf(v)
	// fmt.Printf("%s: %T\n", name, v)
	switch typ.Kind() {
	case reflect.Struct:
		for i := 0; i < typ.NumField(); i++ {
			f := typ.Field(i)
			fieldname := fmt.Sprintf("%s.%s", name, f.Name)
			err := nonZero(fieldname, uniq, val.Field(i).Interface())
			if err != nil {
				return err
			}
		}

	case reflect.Slice:
		if val.Len() == 0 {
			return fmt.Errorf("%q is empty slice", name)
		}
		for i := 0; i < val.Len(); i++ {
			elemname := fmt.Sprintf("%s[%d]", name, i)
			err := nonZero(elemname, uniq, val.Index(i).Interface())
			if err != nil {
				return err
			}
		}

	case reflect.Map:
		if val.Len() == 0 {
			return fmt.Errorf("%q is empty map", name)
		}
		for _, key := range val.MapKeys() {
			keyname := fmt.Sprintf("%s[%s]", name, key.String())
			if err := nonZero(keyname, uniq, key.Interface()); err != nil {
				if strings.Contains(err.Error(), "is zero value") {
					return fmt.Errorf("%q has zero value map key", name)
				}
				return err
			}
			if err := nonZero(keyname, uniq, val.MapIndex(key).Interface()); err != nil {
				return err
			}
		}

	case reflect.Bool:
		if val.Bool() != true {
			return fmt.Errorf("%q is zero value", name)
		}
		// do not test bool for uniqueness since there are only two values

	case reflect.String:
		if val.Len() == 0 {
			return fmt.Errorf("%q is zero value", name)
		}
		return isUnique(v)

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		if val.Int() == 0 {
			return fmt.Errorf("%q is zero value", name)
		}
		return isUnique(v)

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		if val.Uint() == 0 {
			return fmt.Errorf("%q is zero value", name)
		}
		return isUnique(v)

	case reflect.Float32, reflect.Float64:
		if val.Float() == 0 {
			return fmt.Errorf("%q is zero value", name)
		}
		return isUnique(v)

	case reflect.Ptr:
		if val.IsNil() {
			return fmt.Errorf("%q is nil", name)
		}
		return nonZero("*"+name, uniq, val.Elem().Interface())

	default:
		return fmt.Errorf("%T is not supported", v)
	}
	return nil
}

func TestNonZero(t *testing.T) {
	tests := []struct {
		desc string
		v    interface{}
		err  error
	}{
		{"nil", nil, errors.New(`"x" is nil`)},
		{"zero bool", false, errors.New(`"x" is zero value`)},
		{"zero string", "", errors.New(`"x" is zero value`)},
		{"zero int", int(0), errors.New(`"x" is zero value`)},
		{"zero int8", int8(0), errors.New(`"x" is zero value`)},
		{"zero int16", int16(0), errors.New(`"x" is zero value`)},
		{"zero int32", int32(0), errors.New(`"x" is zero value`)},
		{"zero int64", int64(0), errors.New(`"x" is zero value`)},
		{"zero uint", uint(0), errors.New(`"x" is zero value`)},
		{"zero uint8", uint8(0), errors.New(`"x" is zero value`)},
		{"zero uint16", uint16(0), errors.New(`"x" is zero value`)},
		{"zero uint32", uint32(0), errors.New(`"x" is zero value`)},
		{"zero uint64", uint64(0), errors.New(`"x" is zero value`)},
		{"zero float32", float32(0), errors.New(`"x" is zero value`)},
		{"zero float64", float64(0), errors.New(`"x" is zero value`)},
		{"ptr to zero value", pString(""), errors.New(`"*x" is zero value`)},
		{"empty slice", []string{}, errors.New(`"x" is empty slice`)},
		{"slice with zero value", []string{""}, errors.New(`"x[0]" is zero value`)},
		{"empty map", map[string]string{}, errors.New(`"x" is empty map`)},
		{"map with zero value key", map[string]string{"": "y"}, errors.New(`"x" has zero value map key`)},
		{"map with zero value elem", map[string]string{"y": ""}, errors.New(`"x[y]" is zero value`)},
		{"struct with nil field", struct{ Y *int }{}, errors.New(`"x.Y" is nil`)},
		{"struct with zero value field", struct{ Y string }{}, errors.New(`"x.Y" is zero value`)},
		{"struct with empty array", struct{ Y []string }{}, errors.New(`"x.Y" is empty slice`)},
	}

	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			require.Equal(t, tt.err, nonZero("x", nil, tt.v))
		})
	}
}

func TestConfigDecodeBytes(t *testing.T) {
	// Test with some input
	src := []byte("abc")
	key := base64.StdEncoding.EncodeToString(src)

	result, err := decodeBytes(key)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !bytes.Equal(src, result) {
		t.Fatalf("bad: %#v", result)
	}

	// Test with no input
	result, err = decodeBytes("")
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if len(result) > 0 {
		t.Fatalf("bad: %#v", result)
	}
}

func parseCIDR(t *testing.T, cidr string) *net.IPNet {
	_, x, err := net.ParseCIDR(cidr)
	if err != nil {
		t.Fatalf("CIDRParse: %v", err)
	}
	return x
}

func TestRuntimeConfig_Sanitize(t *testing.T) {
	rt := RuntimeConfig{
		BindAddr:             &net.IPAddr{IP: net.ParseIP("127.0.0.1")},
		BuildDate:            time.Date(2019, 11, 20, 5, 0, 0, 0, time.UTC),
		CheckOutputMaxSize:   checks.DefaultBufSize,
		SerfAdvertiseAddrLAN: &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
		DNSAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
			&net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
		},
		DNSSOA: RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0},
		AllowWriteHTTPFrom: []*net.IPNet{
			parseCIDR(t, "127.0.0.0/8"),
			parseCIDR(t, "::1/128"),
		},
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
			&net.UnixAddr{Name: "/var/run/foo"},
		},
		Cache: cache.Options{
			EntryFetchMaxBurst: 42,
			EntryFetchRate:     0.334,
		},
		Cloud: hcpconfig.CloudConfig{
			ResourceID:   "cluster1",
			ClientID:     "id",
			ClientSecret: "secret",
		},
		ConsulCoordinateUpdatePeriod: 15 * time.Second,
		RaftProtocol:                 3,
		RetryJoinLAN: []string{
			"foo=bar key=baz secret=boom bang=bar",
		},
		RetryJoinWAN: []string{
			"wan_foo=bar wan_key=baz wan_secret=boom wan_bang=bar",
		},
		PrimaryGateways: []string{
			"pmgw_foo=bar pmgw_key=baz pmgw_secret=boom pmgw_bang=bar",
		},
		Services: []*structs.ServiceDefinition{
			{
				Name:  "foo",
				Token: "bar",
				Check: structs.CheckType{
					Name:          "blurb",
					OutputMaxSize: checks.DefaultBufSize,
				},
				Weights: &structs.Weights{
					Passing: 67,
					Warning: 3,
				},
			},
		},
		Checks: []*structs.CheckDefinition{
			{
				Name:          "zoo",
				Token:         "zope",
				OutputMaxSize: checks.DefaultBufSize,
			},
		},
		KVMaxValueSize: 1234567800000000,
		SerfAllowedCIDRsLAN: []net.IPNet{
			*parseCIDR(t, "192.168.1.0/24"),
			*parseCIDR(t, "127.0.0.0/8"),
		},
		TxnMaxReqLen: 5678000000000000,
		UIConfig: UIConfig{
			MetricsProxy: UIMetricsProxy{
				AddHeaders: []UIMetricsProxyAddHeader{
					{Name: "foo", Value: "secret"},
				},
			},
		},
		Locality:               &Locality{Region: strPtr("us-west-1"), Zone: strPtr("us-west-1a")},
		ServerRejoinAgeMax:     24 * 7 * time.Hour,
		EnableXDSLoadBalancing: true,
	}

	b, err := json.MarshalIndent(rt.Sanitized(), "", "    ")
	require.NoError(t, err)
	actual := string(b)
	require.JSONEq(t, golden(t, actual, testRuntimeConfigSanitizeExpectedFilename), actual)
}

func TestRuntime_apiAddresses(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678},
			&net.UnixAddr{Name: "/var/run/foo"},
		},
		HTTPSAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("198.18.0.2"), Port: 5678},
		}}

	unixAddrs, httpAddrs, httpsAddrs := rt.apiAddresses(1)

	require.Len(t, unixAddrs, 1)
	require.Len(t, httpAddrs, 1)
	require.Len(t, httpsAddrs, 1)

	require.Equal(t, "/var/run/foo", unixAddrs[0])
	require.Equal(t, "198.18.0.1:5678", httpAddrs[0])
	require.Equal(t, "198.18.0.2:5678", httpsAddrs[0])
}

func TestRuntime_APIConfigHTTPS(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678},
			&net.UnixAddr{Name: "/var/run/foo"},
		},
		HTTPSAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("198.18.0.2"), Port: 5678},
		},
		Datacenter: "dc-test",
		TLS: tlsutil.Config{
			HTTPS: tlsutil.ProtocolConfig{
				CAFile:         "/etc/consul/ca.crt",
				CAPath:         "/etc/consul/ca.dir",
				CertFile:       "/etc/consul/server.crt",
				KeyFile:        "/etc/consul/ssl/server.key",
				VerifyOutgoing: false,
			},
		},
	}

	cfg, err := rt.APIConfig(false)
	require.NoError(t, err)
	require.Equal(t, "198.18.0.2:5678", cfg.Address)
	require.Equal(t, "https", cfg.Scheme)
	require.Equal(t, rt.TLS.HTTPS.CAFile, cfg.TLSConfig.CAFile)
	require.Equal(t, rt.TLS.HTTPS.CAPath, cfg.TLSConfig.CAPath)
	require.Equal(t, "", cfg.TLSConfig.CertFile)
	require.Equal(t, "", cfg.TLSConfig.KeyFile)
	require.Equal(t, rt.Datacenter, cfg.Datacenter)
	require.Equal(t, true, cfg.TLSConfig.InsecureSkipVerify)

	rt.TLS.HTTPS.VerifyOutgoing = true
	cfg, err = rt.APIConfig(true)
	require.NoError(t, err)
	require.Equal(t, "198.18.0.2:5678", cfg.Address)
	require.Equal(t, "https", cfg.Scheme)
	require.Equal(t, rt.TLS.HTTPS.CAFile, cfg.TLSConfig.CAFile)
	require.Equal(t, rt.TLS.HTTPS.CAPath, cfg.TLSConfig.CAPath)
	require.Equal(t, rt.TLS.HTTPS.CertFile, cfg.TLSConfig.CertFile)
	require.Equal(t, rt.TLS.HTTPS.KeyFile, cfg.TLSConfig.KeyFile)
	require.Equal(t, rt.Datacenter, cfg.Datacenter)
	require.Equal(t, false, cfg.TLSConfig.InsecureSkipVerify)
}

func TestRuntime_APIConfigHTTP(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.UnixAddr{Name: "/var/run/foo"},
			&net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678},
		},
		Datacenter:          "dc-test",
		StaticRuntimeConfig: StaticRuntimeConfig{},
	}

	cfg, err := rt.APIConfig(false)
	require.NoError(t, err)
	require.Equal(t, rt.Datacenter, cfg.Datacenter)
	require.Equal(t, "198.18.0.1:5678", cfg.Address)
	require.Equal(t, "http", cfg.Scheme)
	require.Equal(t, "", cfg.TLSConfig.CAFile)
	require.Equal(t, "", cfg.TLSConfig.CAPath)
	require.Equal(t, "", cfg.TLSConfig.CertFile)
	require.Equal(t, "", cfg.TLSConfig.KeyFile)
}

func TestRuntime_APIConfigUNIX(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.UnixAddr{Name: "/var/run/foo"},
		},
		Datacenter: "dc-test",
	}

	cfg, err := rt.APIConfig(false)
	require.NoError(t, err)
	require.Equal(t, rt.Datacenter, cfg.Datacenter)
	require.Equal(t, "unix:///var/run/foo", cfg.Address)
	require.Equal(t, "http", cfg.Scheme)
	require.Equal(t, "", cfg.TLSConfig.CAFile)
	require.Equal(t, "", cfg.TLSConfig.CAPath)
	require.Equal(t, "", cfg.TLSConfig.CertFile)
	require.Equal(t, "", cfg.TLSConfig.KeyFile)
}

func TestRuntime_APIConfigANYAddrV4(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5678},
		},
		Datacenter: "dc-test",
	}

	cfg, err := rt.APIConfig(false)
	require.NoError(t, err)
	require.Equal(t, rt.Datacenter, cfg.Datacenter)
	require.Equal(t, "127.0.0.1:5678", cfg.Address)
	require.Equal(t, "http", cfg.Scheme)
	require.Equal(t, "", cfg.TLSConfig.CAFile)
	require.Equal(t, "", cfg.TLSConfig.CAPath)
	require.Equal(t, "", cfg.TLSConfig.CertFile)
	require.Equal(t, "", cfg.TLSConfig.KeyFile)
}

func TestRuntime_APIConfigANYAddrV6(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("::"), Port: 5678},
		},
		Datacenter: "dc-test",
	}

	cfg, err := rt.APIConfig(false)
	require.NoError(t, err)
	require.Equal(t, rt.Datacenter, cfg.Datacenter)
	require.Equal(t, "[::1]:5678", cfg.Address)
	require.Equal(t, "http", cfg.Scheme)
	require.Equal(t, "", cfg.TLSConfig.CAFile)
	require.Equal(t, "", cfg.TLSConfig.CAPath)
	require.Equal(t, "", cfg.TLSConfig.CertFile)
	require.Equal(t, "", cfg.TLSConfig.KeyFile)
}

func TestRuntime_ClientAddress(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("::"), Port: 5678},
			&net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5679},
			&net.UnixAddr{Name: "/var/run/foo", Net: "unix"},
		},
		HTTPSAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("::"), Port: 5688},
			&net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5689},
		},
	}

	unix, http, https := rt.ClientAddress()

	require.Equal(t, "unix:///var/run/foo", unix)
	require.Equal(t, "198.18.0.1:5679", http)
	require.Equal(t, "198.18.0.1:5689", https)
}

func TestRuntime_ClientAddressAnyV4(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5678},
			&net.UnixAddr{Name: "/var/run/foo", Net: "unix"},
		},
		HTTPSAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5688},
		},
	}

	unix, http, https := rt.ClientAddress()

	require.Equal(t, "unix:///var/run/foo", unix)
	require.Equal(t, "127.0.0.1:5678", http)
	require.Equal(t, "127.0.0.1:5688", https)
}

func TestRuntime_ClientAddressAnyV6(t *testing.T) {
	rt := RuntimeConfig{
		HTTPAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("::"), Port: 5678},
			&net.UnixAddr{Name: "/var/run/foo", Net: "unix"},
		},
		HTTPSAddrs: []net.Addr{
			&net.TCPAddr{IP: net.ParseIP("::"), Port: 5688},
		},
	}

	unix, http, https := rt.ClientAddress()

	require.Equal(t, "unix:///var/run/foo", unix)
	require.Equal(t, "[::1]:5678", http)
	require.Equal(t, "[::1]:5688", https)
}

func Test_UIPathBuilder(t *testing.T) {
	cases := []struct {
		name     string
		path     string
		expected string
	}{
		{
			"Letters only string",
			"hello",
			"/hello/",
		},
		{
			"Alphanumeric",
			"Hello1",
			"/Hello1/",
		},
		{
			"Hyphen and underscore",
			"-_",
			"/-_/",
		},
		{
			"Many slashes",
			"/hello/ui/1/",
			"/hello/ui/1/",
		},
	}

	for _, tt := range cases {
		actual := UIPathBuilder(tt.path)
		require.Equal(t, tt.expected, actual)

	}
}

func splitIPPort(hostport string) (net.IP, int) {
	h, p, err := net.SplitHostPort(hostport)
	if err != nil {
		panic(err)
	}
	port, err := strconv.Atoi(p)
	if err != nil {
		panic(err)
	}
	return net.ParseIP(h), port
}

func ipAddr(addr string) *net.IPAddr {
	return &net.IPAddr{IP: net.ParseIP(addr)}
}

func tcpAddr(addr string) *net.TCPAddr {
	ip, port := splitIPPort(addr)
	return &net.TCPAddr{IP: ip, Port: port}
}

func udpAddr(addr string) *net.UDPAddr {
	ip, port := splitIPPort(addr)
	return &net.UDPAddr{IP: ip, Port: port}
}

func unixAddr(addr string) *net.UnixAddr {
	if !strings.HasPrefix(addr, "unix://") {
		panic("not a unix socket addr: " + addr)
	}
	return &net.UnixAddr{Net: "unix", Name: addr[len("unix://"):]}
}

func writeFile(path string, data []byte) {
	if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
		panic(err)
	}
	if err := os.WriteFile(path, data, 0640); err != nil {
		panic(err)
	}
}

func randomString(n int) string {
	s := ""
	for ; n > 0; n-- {
		s += "x"
	}
	return s
}

func metaPairs(n int, format string) string {
	var s []string
	for i := 0; i < n; i++ {
		switch format {
		case "json":
			s = append(s, fmt.Sprintf(`"%d":"%d"`, i, i))
		case "hcl":
			s = append(s, fmt.Sprintf(`"%d"="%d"`, i, i))
		default:
			panic("invalid format: " + format)
		}
	}
	switch format {
	case "json":
		return strings.Join(s, ",")
	case "hcl":
		return strings.Join(s, " ")
	default:
		panic("invalid format: " + format)
	}
}

func TestConnectCAConfiguration(t *testing.T) {
	type testCase struct {
		config   RuntimeConfig
		expected *structs.CAConfiguration
		err      string
	}

	cases := map[string]testCase{
		"defaults": {
			config: RuntimeConfig{
				ConnectEnabled: true,
			},
			expected: &structs.CAConfiguration{
				Provider: "consul",
				Config: map[string]interface{}{
					"LeafCertTTL":         "72h",
					"IntermediateCertTTL": "8760h",  // 365 * 24h
					"RootCertTTL":         "87600h", // 365 * 10 * 24h
				},
			},
		},
		"cluster-id-override": {
			config: RuntimeConfig{
				ConnectEnabled: true,
				ConnectCAConfig: map[string]interface{}{
					"cluster_id": "adfe7697-09b4-413a-ac0a-fa81ed3a3001",
				},
			},
			expected: &structs.CAConfiguration{
				Provider:  "consul",
				ClusterID: "adfe7697-09b4-413a-ac0a-fa81ed3a3001",
				Config: map[string]interface{}{
					"LeafCertTTL":         "72h",
					"IntermediateCertTTL": "8760h",  // 365 * 24h
					"RootCertTTL":         "87600h", // 365 * 10 * 24h
					"cluster_id":          "adfe7697-09b4-413a-ac0a-fa81ed3a3001",
				},
			},
		},
		"cluster-id-non-uuid": {
			config: RuntimeConfig{
				ConnectEnabled: true,
				ConnectCAConfig: map[string]interface{}{
					"cluster_id": "foo",
				},
			},
			err: "cluster_id was supplied but was not a valid UUID",
		},
		"provider-override": {
			config: RuntimeConfig{
				ConnectEnabled:    true,
				ConnectCAProvider: "vault",
			},
			expected: &structs.CAConfiguration{
				Provider: "vault",
				Config: map[string]interface{}{
					"LeafCertTTL":         "72h",
					"IntermediateCertTTL": "8760h",  // 365 * 24h
					"RootCertTTL":         "87600h", // 365 * 10 * 24h
				},
			},
		},
		"other-config": {
			config: RuntimeConfig{
				ConnectEnabled: true,
				ConnectCAConfig: map[string]interface{}{
					"foo":         "bar",
					"RootCertTTL": "8761h", // 365 * 24h + 1
				},
			},
			expected: &structs.CAConfiguration{
				Provider: "consul",
				Config: map[string]interface{}{
					"LeafCertTTL":         "72h",
					"IntermediateCertTTL": "8760h", // 365 * 24h
					"RootCertTTL":         "8761h", // 365 * 24h + 1
					"foo":                 "bar",
				},
			},
		},
	}

	for name, tcase := range cases {
		t.Run(name, func(t *testing.T) {
			actual, err := tcase.config.ConnectCAConfiguration()
			if tcase.err != "" {
				testutil.RequireErrorContains(t, err, tcase.err)
			} else {
				require.NoError(t, err)
				require.Equal(t, tcase.expected, actual)
			}
		})
	}
}
