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

package api

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/sdk/testutil"
)

func TestAPI_ConfigEntries(t *testing.T) {
	t.Parallel()
	c, s := makeClient(t)
	defer s.Stop()

	config_entries := c.ConfigEntries()

	t.Run("Proxy Defaults", func(t *testing.T) {
		global_proxy := &ProxyConfigEntry{
			Kind: ProxyDefaults,
			Name: ProxyConfigGlobal,
			Config: map[string]interface{}{
				"foo": "bar",
				"bar": 1.0,
			},
			MutualTLSMode: MutualTLSModeStrict,
			Meta: map[string]string{
				"foo": "bar",
				"gir": "zim",
			},
		}

		// set it
		_, wm, err := config_entries.Set(global_proxy, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// get it
		entry, qm, err := config_entries.Get(ProxyDefaults, ProxyConfigGlobal, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)

		// verify it
		readProxy, ok := entry.(*ProxyConfigEntry)
		require.True(t, ok)
		require.Equal(t, global_proxy.Kind, readProxy.Kind)
		require.Equal(t, global_proxy.Name, readProxy.Name)
		require.Equal(t, global_proxy.Config, readProxy.Config)
		require.Equal(t, global_proxy.MutualTLSMode, readProxy.MutualTLSMode)
		require.Equal(t, global_proxy.Meta, readProxy.GetMeta())
		require.Equal(t, global_proxy.Meta, readProxy.GetMeta())

		global_proxy.Config["baz"] = true
		// CAS update fail
		written, _, err := config_entries.CAS(global_proxy, 0, nil)
		require.NoError(t, err)
		require.False(t, written)

		// CAS update success
		written, wm, err = config_entries.CAS(global_proxy, readProxy.ModifyIndex, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)
		require.NoError(t, err)
		require.True(t, written)

		// Non CAS update
		global_proxy.Config["baz"] = "baz"
		_, wm, err = config_entries.Set(global_proxy, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// list it
		entries, qm, err := config_entries.List(ProxyDefaults, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)
		require.Len(t, entries, 1)
		readProxy, ok = entries[0].(*ProxyConfigEntry)
		require.True(t, ok)
		require.Equal(t, global_proxy.Kind, readProxy.Kind)
		require.Equal(t, global_proxy.Name, readProxy.Name)
		require.Equal(t, global_proxy.Config, readProxy.Config)

		// delete it
		wm, err = config_entries.Delete(ProxyDefaults, ProxyConfigGlobal, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		_, _, err = config_entries.Get(ProxyDefaults, ProxyConfigGlobal, nil)
		require.Error(t, err)
	})

	t.Run("Service Defaults", func(t *testing.T) {
		service := &ServiceConfigEntry{
			Kind:          ServiceDefaults,
			Name:          "foo",
			Protocol:      "http",
			MutualTLSMode: MutualTLSModeStrict,
			Meta: map[string]string{
				"foo": "bar",
				"gir": "zim",
			},
			MaxInboundConnections:     5,
			BalanceInboundConnections: "exact_balance",
			LocalConnectTimeoutMs:     5000,
			LocalRequestTimeoutMs:     7000,
		}

		dest := &DestinationConfig{
			Addresses: []string{"my.example.com"},
			Port:      80,
		}

		service2 := &ServiceConfigEntry{
			Kind:        ServiceDefaults,
			Name:        "bar",
			Protocol:    "http",
			Destination: dest,
		}

		// set it
		_, wm, err := config_entries.Set(service, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// also set the second one
		_, wm, err = config_entries.Set(service2, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// get it
		entry, qm, err := config_entries.Get(ServiceDefaults, "foo", nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)

		// verify it
		readService, ok := entry.(*ServiceConfigEntry)
		require.True(t, ok)
		require.Equal(t, service.Kind, readService.Kind)
		require.Equal(t, service.Name, readService.Name)
		require.Equal(t, service.Protocol, readService.Protocol)
		require.Equal(t, service.MutualTLSMode, readService.MutualTLSMode)
		require.Equal(t, service.Meta, readService.Meta)
		require.Equal(t, service.Meta, readService.GetMeta())
		require.Equal(t, service.MaxInboundConnections, readService.MaxInboundConnections)
		require.Equal(t, service.BalanceInboundConnections, readService.BalanceInboundConnections)
		require.Equal(t, service.LocalConnectTimeoutMs, readService.LocalConnectTimeoutMs)
		require.Equal(t, service.LocalRequestTimeoutMs, readService.LocalRequestTimeoutMs)

		// update it
		service.Protocol = "tcp"

		// CAS fail
		written, _, err := config_entries.CAS(service, 0, nil)
		require.NoError(t, err)
		require.False(t, written)

		// CAS success
		written, wm, err = config_entries.CAS(service, readService.ModifyIndex, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)
		require.True(t, written)

		// update no cas
		service.Protocol = "tcp"

		_, wm, err = config_entries.Set(service, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// list them
		entries, qm, err := config_entries.List(ServiceDefaults, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)
		require.Len(t, entries, 2)

		for _, entry = range entries {
			switch entry.GetName() {
			case "foo":
				// this also verifies that the update value was persisted and
				// the updated values are seen
				readService, ok = entry.(*ServiceConfigEntry)
				require.True(t, ok)
				require.Equal(t, service.Kind, readService.Kind)
				require.Equal(t, service.Name, readService.Name)
				require.Equal(t, service.Protocol, readService.Protocol)
			case "bar":
				readService, ok = entry.(*ServiceConfigEntry)
				require.True(t, ok)
				require.Equal(t, service2.Kind, readService.Kind)
				require.Equal(t, service2.Name, readService.Name)
				require.Equal(t, service2.Protocol, readService.Protocol)
				require.Equal(t, dest, readService.Destination)
			}
		}

		// delete it
		wm, err = config_entries.Delete(ServiceDefaults, "foo", nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// verify deletion
		_, _, err = config_entries.Get(ServiceDefaults, "foo", nil)
		require.Error(t, err)
	})

	t.Run("Mesh", func(t *testing.T) {
		mesh := &MeshConfigEntry{
			TransparentProxy:                 TransparentProxyMeshConfig{MeshDestinationsOnly: true},
			AllowEnablingPermissiveMutualTLS: true,
			Meta: map[string]string{
				"foo": "bar",
				"gir": "zim",
			},
			Partition: defaultPartition,
			Namespace: defaultNamespace,
		}
		ce := c.ConfigEntries()

		testutil.RunStep(t, "set and get", func(t *testing.T) {
			_, wm, err := ce.Set(mesh, nil)
			require.NoError(t, err)
			require.NotNil(t, wm)
			require.NotEqual(t, 0, wm.RequestTime)

			entry, qm, err := ce.Get(MeshConfig, MeshConfigMesh, nil)
			require.NoError(t, err)
			require.NotNil(t, qm)
			require.NotEqual(t, 0, qm.RequestTime)

			result, ok := entry.(*MeshConfigEntry)
			require.True(t, ok)

			// ignore indexes
			result.CreateIndex = 0
			result.ModifyIndex = 0
			require.Equal(t, mesh, result)
		})

		testutil.RunStep(t, "list", func(t *testing.T) {
			entries, qm, err := ce.List(MeshConfig, nil)
			require.NoError(t, err)
			require.NotNil(t, qm)
			require.NotEqual(t, 0, qm.RequestTime)
			require.Len(t, entries, 1)
		})

		testutil.RunStep(t, "delete", func(t *testing.T) {
			wm, err := ce.Delete(MeshConfig, MeshConfigMesh, nil)
			require.NoError(t, err)
			require.NotNil(t, wm)
			require.NotEqual(t, 0, wm.RequestTime)

			// verify deletion
			_, _, err = ce.Get(MeshConfig, MeshConfigMesh, nil)
			require.Error(t, err)
		})
	})

	t.Run("CAS deletion", func(t *testing.T) {

		entry := &ProxyConfigEntry{
			Kind: ProxyDefaults,
			Name: ProxyConfigGlobal,
			Config: map[string]interface{}{
				"foo": "bar",
			},
		}

		// Create a config entry.
		created, _, err := config_entries.Set(entry, nil)
		require.NoError(t, err)
		require.True(t, created, "entry should have been created")

		// Read it back to get the ModifyIndex.
		result, _, err := config_entries.Get(entry.Kind, entry.Name, nil)
		require.NoError(t, err)
		require.NotNil(t, entry)

		// Attempt a deletion with an invalid index.
		deleted, _, err := config_entries.DeleteCAS(entry.Kind, entry.Name, result.GetModifyIndex()-1, nil)
		require.NoError(t, err)
		require.False(t, deleted, "entry should not have been deleted")

		// Attempt a deletion with a valid index.
		deleted, _, err = config_entries.DeleteCAS(entry.Kind, entry.Name, result.GetModifyIndex(), nil)
		require.NoError(t, err)
		require.True(t, deleted, "entry should have been deleted")
	})
}

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

	for _, tc := range []struct {
		name      string
		body      string
		expect    ConfigEntry
		expectErr string
	}{
		{
			name: "expose-paths: kitchen sink proxy",
			body: `
			{
				"Kind": "proxy-defaults",
				"Name": "global",
				"Expose": {
					"Checks": true,
					"Paths": [
						{
							"LocalPathPort": 8080,
							"ListenerPort": 21500,
							"Path": "/healthz",
							"Protocol": "http2"
						}
					]
				}
			}
			`,
			expect: &ProxyConfigEntry{
				Kind: "proxy-defaults",
				Name: "global",
				Expose: ExposeConfig{
					Checks: true,
					Paths: []ExposePath{
						{
							LocalPathPort: 8080,
							ListenerPort:  21500,
							Path:          "/healthz",
							Protocol:      "http2",
						},
					},
				},
			},
		},
		{
			name: "expose-paths: kitchen sink service default",
			body: `
			{
				"Kind": "service-defaults",
				"Name": "global",
				"Expose": {
					"Checks": true,
					"Paths": [
						{
							"LocalPathPort": 8080,
							"ListenerPort": 21500,
							"Path": "/healthz",
							"Protocol": "http2"
						}
					]
				}
			}
			`,
			expect: &ServiceConfigEntry{
				Kind: "service-defaults",
				Name: "global",
				Expose: ExposeConfig{
					Checks: true,
					Paths: []ExposePath{
						{
							LocalPathPort: 8080,
							ListenerPort:  21500,
							Path:          "/healthz",
							Protocol:      "http2",
						},
					},
				},
			},
		},
		{
			name: "proxy-defaults",
			body: `
			{
				"Kind": "proxy-defaults",
				"Name": "main",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"Config": {
				  "foo": 19,
				  "bar": "abc",
				  "moreconfig": {
					"moar": "config"
				  }
				},
				"MeshGateway": {
					"Mode": "remote"
				},
				"Mode": "transparent",
				"TransparentProxy": {
					"OutboundListenerPort": 808,
					"DialedDirectly": true
				},
				"AccessLogs": {
					"Enabled": true,
					"DisableListenerLogs": true,
					"Type": "file",
					"Path": "/tmp/logs.txt",
					"TextFormat": "[%START_TIME%]"
				},
				"FailoverPolicy": {
					"Mode": "default"
				}
			}
			`,
			expect: &ProxyConfigEntry{
				Kind: "proxy-defaults",
				Name: "main",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				Config: map[string]interface{}{
					"foo": float64(19),
					"bar": "abc",
					"moreconfig": map[string]interface{}{
						"moar": "config",
					},
				},
				MeshGateway: MeshGatewayConfig{
					Mode: MeshGatewayModeRemote,
				},
				Mode: ProxyModeTransparent,
				TransparentProxy: &TransparentProxyConfig{
					OutboundListenerPort: 808,
					DialedDirectly:       true,
				},
				AccessLogs: &AccessLogsConfig{
					Enabled:             true,
					DisableListenerLogs: true,
					Type:                FileLogSinkType,
					Path:                "/tmp/logs.txt",
					TextFormat:          "[%START_TIME%]",
				},
				FailoverPolicy: &ServiceResolverFailoverPolicy{
					Mode: "default",
				},
			},
		},
		{
			name: "service-defaults",
			body: `
			{
				"Kind": "service-defaults",
				"Name": "main",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"Protocol": "http",
				"ExternalSNI": "abc-123",
				"MeshGateway": {
					"Mode": "remote"
				},
				"Mode": "transparent",
				"TransparentProxy": {
					"OutboundListenerPort": 808,
					"DialedDirectly": true
				},
				"BalanceInboundConnections": "exact_balance",
				"UpstreamConfig": {
					"Overrides": [
						{
							"Name": "redis",
							"PassiveHealthCheck": {
								"MaxFailures": 3,
								"Interval": "2s",
								"EnforcingConsecutive5xx": 60,
								"MaxEjectionPercent": 4,
								"BaseEjectionTime": "5s"
							},
							"BalanceOutboundConnections": "exact_balance"
						},
						{
							"Name": "finance--billing",
							"MeshGateway": {
								"Mode": "remote"
							}
						}
					],
					"Defaults": {
						"EnvoyClusterJSON": "zip",
						"EnvoyListenerJSON": "zop",
						"ConnectTimeoutMs": 5000,
						"Protocol": "http",
						"Limits": {
							"MaxConnections": 3,
							"MaxPendingRequests": 4,
							"MaxConcurrentRequests": 5
						},
						"PassiveHealthCheck": {
								"MaxFailures": 5,
								"Interval": "4s",
								"EnforcingConsecutive5xx": 61,
								"MaxEjectionPercent": 5,
								"BaseEjectionTime": "6s"

						}
					}
				}
			}
			`,
			expect: &ServiceConfigEntry{
				Kind: "service-defaults",
				Name: "main",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				Protocol:    "http",
				ExternalSNI: "abc-123",
				MeshGateway: MeshGatewayConfig{
					Mode: MeshGatewayModeRemote,
				},
				Mode: ProxyModeTransparent,
				TransparentProxy: &TransparentProxyConfig{
					OutboundListenerPort: 808,
					DialedDirectly:       true,
				},
				BalanceInboundConnections: "exact_balance",
				UpstreamConfig: &UpstreamConfiguration{
					Overrides: []*UpstreamConfig{
						{
							Name: "redis",
							PassiveHealthCheck: &PassiveHealthCheck{
								MaxFailures:             3,
								Interval:                2 * time.Second,
								EnforcingConsecutive5xx: uint32Pointer(60),
								MaxEjectionPercent:      uint32Pointer(4),
								BaseEjectionTime:        durationPointer(5 * time.Second),
							},
							BalanceOutboundConnections: "exact_balance",
						},
						{
							Name:        "finance--billing",
							MeshGateway: MeshGatewayConfig{Mode: "remote"},
						},
					},
					Defaults: &UpstreamConfig{
						EnvoyClusterJSON:  "zip",
						EnvoyListenerJSON: "zop",
						Protocol:          "http",
						ConnectTimeoutMs:  5000,
						Limits: &UpstreamLimits{
							MaxConnections:        intPointer(3),
							MaxPendingRequests:    intPointer(4),
							MaxConcurrentRequests: intPointer(5),
						},
						PassiveHealthCheck: &PassiveHealthCheck{
							MaxFailures:             5,
							Interval:                4 * time.Second,
							EnforcingConsecutive5xx: uint32Pointer(61),
							MaxEjectionPercent:      uint32Pointer(5),
							BaseEjectionTime:        durationPointer(6 * time.Second),
						},
					},
				},
			},
		},
		{
			name: "service-defaults-endpoint",
			body: `
			{
				"Kind": "service-defaults",
				"Name": "external",
				"Protocol": "http",
				"Destination": {
					"Addresses": [
						"1.2.3.4"
					],
					"Port": 443
				}
			}
			`,
			expect: &ServiceConfigEntry{
				Kind:     "service-defaults",
				Name:     "external",
				Protocol: "http",
				Destination: &DestinationConfig{
					Addresses: []string{"1.2.3.4"},
					Port:      443,
				},
			},
		},
		{
			name: "service-router: kitchen sink",
			body: `
			{
				"Kind": "service-router",
				"Name": "main",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"Routes": [
					{
						"Match": {
							"HTTP": {
								"PathExact": "/foo",
								"Header": [
									{
										"Name": "debug1",
										"Present": true
									},
									{
										"Name": "debug2",
										"Present": false,
										"Invert": true
									},
									{
										"Name": "debug3",
										"Exact": "1"
									},
									{
										"Name": "debug4",
										"Prefix": "aaa"
									},
									{
										"Name": "debug5",
										"Suffix": "bbb"
									},
									{
										"Name": "debug6",
										"Regex": "a.*z"
									}
								]
							}
						},
						"Destination": {
						  "Service": "carrot",
						  "ServiceSubset": "kale",
						  "Namespace": "leek",
						  "PrefixRewrite": "/alternate",
						  "RequestTimeout": "99s",
						  "IdleTimeout": "99s",
						  "NumRetries": 12345,
						  "RetryOnConnectFailure": true,
						  "RetryOnStatusCodes": [401, 209]
						}
					},
					{
						"Match": {
							"HTTP": {
								"PathPrefix": "/foo",
								"Methods": [ "GET", "DELETE" ],
								"QueryParam": [
									{
										"Name": "hack1",
										"Present": true
									},
									{
										"Name": "hack2",
										"Exact": "1"
									},
									{
										"Name": "hack3",
										"Regex": "a.*z"
									}
								]
							}
						}
					},
					{
						"Match": {
							"HTTP": {
								"PathRegex": "/foo"
							}
						}
					}
				]
			}
			`,
			expect: &ServiceRouterConfigEntry{
				Kind: "service-router",
				Name: "main",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				Routes: []ServiceRoute{
					{
						Match: &ServiceRouteMatch{
							HTTP: &ServiceRouteHTTPMatch{
								PathExact: "/foo",
								Header: []ServiceRouteHTTPMatchHeader{
									{
										Name:    "debug1",
										Present: true,
									},
									{
										Name:    "debug2",
										Present: false,
										Invert:  true,
									},
									{
										Name:  "debug3",
										Exact: "1",
									},
									{
										Name:   "debug4",
										Prefix: "aaa",
									},
									{
										Name:   "debug5",
										Suffix: "bbb",
									},
									{
										Name:  "debug6",
										Regex: "a.*z",
									},
								},
							},
						},
						Destination: &ServiceRouteDestination{
							Service:               "carrot",
							ServiceSubset:         "kale",
							Namespace:             "leek",
							PrefixRewrite:         "/alternate",
							RequestTimeout:        99 * time.Second,
							IdleTimeout:           99 * time.Second,
							NumRetries:            12345,
							RetryOnConnectFailure: true,
							RetryOnStatusCodes:    []uint32{401, 209},
						},
					},
					{
						Match: &ServiceRouteMatch{
							HTTP: &ServiceRouteHTTPMatch{
								PathPrefix: "/foo",
								Methods:    []string{"GET", "DELETE"},
								QueryParam: []ServiceRouteHTTPMatchQueryParam{
									{
										Name:    "hack1",
										Present: true,
									},
									{
										Name:  "hack2",
										Exact: "1",
									},
									{
										Name:  "hack3",
										Regex: "a.*z",
									},
								},
							},
						},
					},
					{
						Match: &ServiceRouteMatch{
							HTTP: &ServiceRouteHTTPMatch{
								PathRegex: "/foo",
							},
						},
					},
				},
			},
		},
		{
			name: "service-splitter: kitchen sink",
			body: `
			{
				"Kind": "service-splitter",
				"Name": "main",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"Splits": [
				  {
					"Weight": 99.1,
					"ServiceSubset": "v1"
				  },
				  {
					"Weight": 0.9,
					"Service": "other",
					"Namespace": "alt"
				  }
				]
			}
			`,
			expect: &ServiceSplitterConfigEntry{
				Kind: ServiceSplitter,
				Name: "main",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				Splits: []ServiceSplit{
					{
						Weight:        99.1,
						ServiceSubset: "v1",
					},
					{
						Weight:    0.9,
						Service:   "other",
						Namespace: "alt",
					},
				},
			},
		},
		{
			name: "service-resolver: subsets with failover",
			body: `
			{
				"Kind": "service-resolver",
				"Name": "main",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"DefaultSubset": "v1",
				"ConnectTimeout": "15s",
				"Subsets": {
					"v1": {
						"Filter": "Service.Meta.version == v1"
					},
					"v2": {
						"Filter": "Service.Meta.version == v2",
						"OnlyPassing": true
					}
				},
				"Failover": {
					"v2": {
						"Service": "failcopy",
						"ServiceSubset": "sure",
						"Namespace": "neighbor",
						"Datacenters": ["dc5", "dc14"]
					},
					"*": {
						"Datacenters": ["dc7"]
					}
				}
			}`,
			expect: &ServiceResolverConfigEntry{
				Kind: "service-resolver",
				Name: "main",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				DefaultSubset:  "v1",
				ConnectTimeout: 15 * time.Second,
				Subsets: map[string]ServiceResolverSubset{
					"v1": {
						Filter: "Service.Meta.version == v1",
					},
					"v2": {
						Filter:      "Service.Meta.version == v2",
						OnlyPassing: true,
					},
				},
				Failover: map[string]ServiceResolverFailover{
					"v2": {
						Service:       "failcopy",
						ServiceSubset: "sure",
						Namespace:     "neighbor",
						Datacenters:   []string{"dc5", "dc14"},
					},
					"*": {
						Datacenters: []string{"dc7"},
					},
				},
			},
		},
		{
			name: "service-resolver: redirect",
			body: `
			{
				"Kind": "service-resolver",
				"Name": "main",
				"Redirect": {
					"Service": "other",
					"ServiceSubset": "backup",
					"Namespace": "alt",
					"Datacenter": "dc9"
				}
			}
			`,
			expect: &ServiceResolverConfigEntry{
				Kind: "service-resolver",
				Name: "main",
				Redirect: &ServiceResolverRedirect{
					Service:       "other",
					ServiceSubset: "backup",
					Namespace:     "alt",
					Datacenter:    "dc9",
				},
			},
		},
		{
			name: "service-resolver: default",
			body: `
			{
				"Kind": "service-resolver",
				"Name": "main"
			}
			`,
			expect: &ServiceResolverConfigEntry{
				Kind: "service-resolver",
				Name: "main",
			},
		},
		{
			name: "service-resolver: envoy hash lb kitchen sink",
			body: `
			{
				"Kind": "service-resolver",
				"Name": "main",
				"LoadBalancer": {
					"Policy": "ring_hash",
					"RingHashConfig": {
						"MinimumRingSize": 1,
						"MaximumRingSize": 2
					},
					"HashPolicies": [
						{
							"Field": "cookie",
							"FieldValue": "good-cookie",
							"CookieConfig": {
								"TTL": "1s",
								"Path": "/oven"
							},
							"Terminal": true
						},
						{
							"Field": "cookie",
							"FieldValue": "less-good-cookie",
							"CookieConfig": {
								"Session": true,
								"Path": "/toaster"
							},
							"Terminal": true
						},
						{
							"Field": "header",
							"FieldValue": "x-user-id"
						},
						{
							"SourceIP": true
						}
					]
				}
			}
			`,
			expect: &ServiceResolverConfigEntry{
				Kind: "service-resolver",
				Name: "main",
				LoadBalancer: &LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &RingHashConfig{
						MinimumRingSize: 1,
						MaximumRingSize: 2,
					},
					HashPolicies: []HashPolicy{
						{
							Field:      "cookie",
							FieldValue: "good-cookie",
							CookieConfig: &CookieConfig{
								TTL:  1 * time.Second,
								Path: "/oven",
							},
							Terminal: true,
						},
						{
							Field:      "cookie",
							FieldValue: "less-good-cookie",
							CookieConfig: &CookieConfig{
								Session: true,
								Path:    "/toaster",
							},
							Terminal: true,
						},
						{
							Field:      "header",
							FieldValue: "x-user-id",
						},
						{
							SourceIP: true,
						},
					},
				},
			},
		},
		{
			name: "service-resolver: envoy least request kitchen sink",
			body: `
			{
				"Kind": "service-resolver",
				"Name": "main",
				"LoadBalancer": {
					"Policy": "least_request",
					"LeastRequestConfig": {
						"ChoiceCount": 2
					}
				}
			}
			`,
			expect: &ServiceResolverConfigEntry{
				Kind: "service-resolver",
				Name: "main",
				LoadBalancer: &LoadBalancer{
					Policy: "least_request",
					LeastRequestConfig: &LeastRequestConfig{
						ChoiceCount: 2,
					},
				},
			},
		},
		{
			// TODO(rb): test SDS stuff here in both places (global/service)
			name: "ingress-gateway",
			body: `
			{
				"Kind": "ingress-gateway",
				"Name": "ingress-web",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"Tls": {
					"Enabled": true,
					"TLSMinVersion": "TLSv1_1",
					"TLSMaxVersion": "TLSv1_2",
					"CipherSuites": [
						"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
						"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
					]
				},
				"Listeners": [
					{
						"Port": 8080,
						"Protocol": "http",
						"Services": [
							{
								"Name": "web",
								"Namespace": "foo",
								"Partition": "bar"
							},
							{
								"Name": "db"
							}
						]
					},
					{
						"Port": 9999,
						"Protocol": "tcp",
						"Services": [
							{
								"Name": "mysql"
							}
						]
					}
				]
			}
			`,
			expect: &IngressGatewayConfigEntry{
				Kind: "ingress-gateway",
				Name: "ingress-web",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				TLS: GatewayTLSConfig{
					Enabled:       true,
					TLSMinVersion: "TLSv1_1",
					TLSMaxVersion: "TLSv1_2",
					CipherSuites: []string{
						"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
						"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
					},
				},
				Listeners: []IngressListener{
					{
						Port:     8080,
						Protocol: "http",
						Services: []IngressService{
							{
								Name:      "web",
								Namespace: "foo",
								Partition: "bar",
							},
							{
								Name: "db",
							},
						},
					},
					{
						Port:     9999,
						Protocol: "tcp",
						Services: []IngressService{
							{
								Name: "mysql",
							},
						},
					},
				},
			},
		},
		{
			name: "terminating-gateway",
			body: `
			{
				"Kind": "terminating-gateway",
				"Name": "terminating-west",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"Services": [
					{
						"Namespace": "foo",
						"Name": "web",
						"CAFile": "/etc/ca.pem",
						"CertFile": "/etc/cert.pem",
						"KeyFile": "/etc/tls.key",
						"SNI": "mydomain"
					},
					{
						"Name": "api"
					},
					{
						"Namespace": "bar",
						"Name": "*"
					}
				]
			}`,
			expect: &TerminatingGatewayConfigEntry{
				Kind: "terminating-gateway",
				Name: "terminating-west",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				Services: []LinkedService{
					{
						Namespace: "foo",
						Name:      "web",
						CAFile:    "/etc/ca.pem",
						CertFile:  "/etc/cert.pem",
						KeyFile:   "/etc/tls.key",
						SNI:       "mydomain",
					},
					{
						Name: "api",
					},
					{
						Namespace: "bar",
						Name:      "*",
					},
				},
			},
		},
		{
			name: "service-intentions: kitchen sink",
			body: `
			{
				"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": "l7",
						"Permissions": [
							{
								"Action": "deny",
								"HTTP": {
									"PathExact": "/admin",
									"Header": [
										{
											"Name": "hdr-present",
											"Present": true
										},
										{
											"Name": "hdr-exact",
											"Exact": "exact"
										},
										{
											"Name": "hdr-prefix",
											"Prefix": "prefix"
										},
										{
											"Name": "hdr-suffix",
											"Suffix": "suffix"
										},
										{
											"Name": "hdr-regex",
											"Regex": "regex"
										},
										{
											"Name": "hdr-absent",
											"Present": true,
											"Invert": true
										}
									]
								}
							},
							{
								"Action": "allow",
								"HTTP": {
									"PathPrefix": "/v3/"
								}
							},
							{
								"Action": "allow",
								"HTTP": {
									"PathRegex": "/v[12]/.*",
									"Methods": [
										"GET",
										"POST"
									]
								}
							}
						]
					},
					{
						"Name": "*",
						"Action": "deny",
						"Description": "wild desc"
					}
				]
			}
			`,
			expect: &ServiceIntentionsConfigEntry{
				Kind: "service-intentions",
				Name: "web",
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				Sources: []*SourceIntention{
					{
						Name:        "foo",
						Action:      "deny",
						Type:        "consul",
						Description: "foo desc",
					},
					{
						Name:        "bar",
						Action:      "allow",
						Description: "bar desc",
					},
					{
						Name: "l7",
						Permissions: []*IntentionPermission{
							{
								Action: "deny",
								HTTP: &IntentionHTTPPermission{
									PathExact: "/admin",
									Header: []IntentionHTTPHeaderPermission{
										{
											Name:    "hdr-present",
											Present: true,
										},
										{
											Name:  "hdr-exact",
											Exact: "exact",
										},
										{
											Name:   "hdr-prefix",
											Prefix: "prefix",
										},
										{
											Name:   "hdr-suffix",
											Suffix: "suffix",
										},
										{
											Name:  "hdr-regex",
											Regex: "regex",
										},
										{
											Name:    "hdr-absent",
											Present: true,
											Invert:  true,
										},
									},
								},
							},
							{
								Action: "allow",
								HTTP: &IntentionHTTPPermission{
									PathPrefix: "/v3/",
								},
							},
							{
								Action: "allow",
								HTTP: &IntentionHTTPPermission{
									PathRegex: "/v[12]/.*",
									Methods:   []string{"GET", "POST"},
								},
							},
						},
					},
					{
						Name:        "*",
						Action:      "deny",
						Description: "wild desc",
					},
				},
			},
		},
		{
			name: "mesh",
			body: `
			{
				"Kind": "mesh",
				"Meta" : {
					"foo": "bar",
					"gir": "zim"
				},
				"TransparentProxy": {
					"MeshDestinationsOnly": true
				},
				"TLS": {
					"Incoming": {
						"TLSMinVersion": "TLSv1_1",
						"TLSMaxVersion": "TLSv1_2",
						"CipherSuites": [
							"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
							"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
						]
					},
					"Outgoing": {
						"TLSMinVersion": "TLSv1_1",
						"TLSMaxVersion": "TLSv1_2",
						"CipherSuites": [
							"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
							"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
						]
					}
				},
				"HTTP": {
					"SanitizeXForwardedClientCert": true
				},
				"Peering": {
					"PeerThroughMeshGateways": true
				}
			}
			`,
			expect: &MeshConfigEntry{
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
				TransparentProxy: TransparentProxyMeshConfig{
					MeshDestinationsOnly: true,
				},
				TLS: &MeshTLSConfig{
					Incoming: &MeshDirectionalTLSConfig{
						TLSMinVersion: "TLSv1_1",
						TLSMaxVersion: "TLSv1_2",
						CipherSuites: []string{
							"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
							"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
						},
					},
					Outgoing: &MeshDirectionalTLSConfig{
						TLSMinVersion: "TLSv1_1",
						TLSMaxVersion: "TLSv1_2",
						CipherSuites: []string{
							"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
							"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
						},
					},
				},
				HTTP: &MeshHTTPConfig{
					SanitizeXForwardedClientCert: true,
				},
				Peering: &PeeringMeshConfig{
					PeerThroughMeshGateways: true,
				},
			},
		},
	} {
		tc := tc

		t.Run(tc.name+": DecodeConfigEntry", func(t *testing.T) {
			var raw map[string]interface{}
			require.NoError(t, json.Unmarshal([]byte(tc.body), &raw))

			got, err := DecodeConfigEntry(raw)
			require.NoError(t, err)
			require.Equal(t, tc.expect, got)
		})

		t.Run(tc.name+": DecodeConfigEntryFromJSON", func(t *testing.T) {
			got, err := DecodeConfigEntryFromJSON([]byte(tc.body))
			require.NoError(t, err)
			require.Equal(t, tc.expect, got)
		})

		t.Run(tc.name+": DecodeConfigEntrySlice", func(t *testing.T) {
			var raw []map[string]interface{}
			require.NoError(t, json.Unmarshal([]byte("["+tc.body+"]"), &raw))

			got, err := decodeConfigEntrySlice(raw)
			require.NoError(t, err)
			require.Len(t, got, 1)
			require.Equal(t, tc.expect, got[0])
		})
	}
}

func intPointer(v int) *int {
	return &v
}

func uint32Pointer(v uint32) *uint32 {
	return &v
}

func durationPointer(d time.Duration) *time.Duration {
	return &d
}
