package structs

import (
	"bytes"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/consul/acl"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) {
	// This test tests both of these because they are related functions.

	newServiceACL := func(t *testing.T, canRead, canWrite []string) acl.Authorizer {
		var buf bytes.Buffer
		for _, s := range canRead {
			buf.WriteString(fmt.Sprintf("service %q { policy = %q }\n", s, "read"))
		}
		for _, s := range canWrite {
			buf.WriteString(fmt.Sprintf("service %q { policy = %q }\n", s, "write"))
		}

		policy, err := acl.NewPolicyFromSource("", 0, buf.String(), acl.SyntaxCurrent, nil, nil)
		require.NoError(t, err)

		authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
		require.NoError(t, err)
		return authorizer
	}

	type testACL struct {
		name       string
		authorizer acl.Authorizer
		canRead    bool
		canWrite   bool
	}

	defaultDenyCase := testACL{
		name:       "deny",
		authorizer: newServiceACL(t, nil, nil),
		canRead:    false,
		canWrite:   false,
	}
	readTestCase := testACL{
		name:       "can read test",
		authorizer: newServiceACL(t, []string{"test"}, nil),
		canRead:    true,
		canWrite:   false,
	}
	writeTestCase := testACL{
		name:       "can write test",
		authorizer: newServiceACL(t, nil, []string{"test"}),
		canRead:    true,
		canWrite:   true,
	}
	writeTestCaseDenied := testACL{
		name:       "cannot write test",
		authorizer: newServiceACL(t, nil, []string{"test"}),
		canRead:    true,
		canWrite:   false,
	}

	for _, tc := range []struct {
		name           string
		entry          discoveryChainConfigEntry
		expectServices []ServiceID
		expectACLs     []testACL
	}{
		{
			name: "resolver: self",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
			},
			expectServices: nil,
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCase,
			},
		},
		{
			name: "resolver: redirect",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Redirect: &ServiceResolverRedirect{
					Service: "other",
				},
			},
			expectServices: []ServiceID{NewServiceID("other", nil)},
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCaseDenied,
				{
					name:       "can write test (with other:read)",
					authorizer: newServiceACL(t, []string{"other"}, []string{"test"}),
					canRead:    true,
					canWrite:   true,
				},
			},
		},
		{
			name: "resolver: failover",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					"foo": {OnlyPassing: true},
					"bar": {OnlyPassing: true},
				},
				Failover: map[string]ServiceResolverFailover{
					"foo": {
						Service: "other1",
					},
					"bar": {
						Service: "other2",
					},
				},
			},
			expectServices: []ServiceID{NewServiceID("other1", nil), NewServiceID("other2", nil)},
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCaseDenied,
				{
					name:       "can write test (with other1:read and other2:read)",
					authorizer: newServiceACL(t, []string{"other1", "other2"}, []string{"test"}),
					canRead:    true,
					canWrite:   true,
				},
			},
		},
		{
			name: "splitter: self",
			entry: &ServiceSplitterConfigEntry{
				Kind: ServiceSplitter,
				Name: "test",
				Splits: []ServiceSplit{
					{Weight: 100},
				},
			},
			expectServices: nil,
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCase,
			},
		},
		{
			name: "splitter: some",
			entry: &ServiceSplitterConfigEntry{
				Kind: ServiceSplitter,
				Name: "test",
				Splits: []ServiceSplit{
					{Weight: 25, Service: "b"},
					{Weight: 25, Service: "a"},
					{Weight: 50, Service: "c"},
				},
			},
			expectServices: []ServiceID{NewServiceID("a", nil), NewServiceID("b", nil), NewServiceID("c", nil)},
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCaseDenied,
				{
					name:       "can write test (with a:read, b:read, and c:read)",
					authorizer: newServiceACL(t, []string{"a", "b", "c"}, []string{"test"}),
					canRead:    true,
					canWrite:   true,
				},
			},
		},
		{
			name: "router: self",
			entry: &ServiceRouterConfigEntry{
				Kind: ServiceRouter,
				Name: "test",
			},
			expectServices: []ServiceID{NewServiceID("test", nil)},
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCase,
			},
		},
		{
			name: "router: some",
			entry: &ServiceRouterConfigEntry{
				Kind: ServiceRouter,
				Name: "test",
				Routes: []ServiceRoute{
					{
						Match: &ServiceRouteMatch{HTTP: &ServiceRouteHTTPMatch{
							PathPrefix: "/foo",
						}},
						Destination: &ServiceRouteDestination{
							Service: "foo",
						},
					},
					{
						Match: &ServiceRouteMatch{HTTP: &ServiceRouteHTTPMatch{
							PathPrefix: "/bar",
						}},
						Destination: &ServiceRouteDestination{
							Service: "bar",
						},
					},
				},
			},
			expectServices: []ServiceID{NewServiceID("bar", nil), NewServiceID("foo", nil), NewServiceID("test", nil)},
			expectACLs: []testACL{
				defaultDenyCase,
				readTestCase,
				writeTestCaseDenied,
				{
					name:       "can write test (with foo:read and bar:read)",
					authorizer: newServiceACL(t, []string{"foo", "bar"}, []string{"test"}),
					canRead:    true,
					canWrite:   true,
				},
			},
		},
	} {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			// sanity check inputs
			require.NoError(t, tc.entry.Normalize())
			require.NoError(t, tc.entry.Validate())

			got := tc.entry.ListRelatedServices()
			require.Equal(t, tc.expectServices, got)

			for _, a := range tc.expectACLs {
				a := a
				t.Run(a.name, func(t *testing.T) {
					require.Equal(t, a.canRead, tc.entry.CanRead(a.authorizer))
					require.Equal(t, a.canWrite, tc.entry.CanWrite(a.authorizer))
				})
			}
		})
	}
}

func TestServiceResolverConfigEntry(t *testing.T) {

	type testcase struct {
		name         string
		entry        *ServiceResolverConfigEntry
		normalizeErr string
		validateErr  string
		// check is called between normalize and validate
		check func(t *testing.T, entry *ServiceResolverConfigEntry)
	}

	cases := []testcase{
		{
			name:         "nil",
			entry:        nil,
			normalizeErr: "config entry is nil",
		},
		{
			name:        "no name",
			entry:       &ServiceResolverConfigEntry{},
			validateErr: "Name is required",
		},
		{
			name: "empty",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
			},
		},
		{
			name: "empty subset name",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					"": {OnlyPassing: true},
				},
			},
			validateErr: "Subset defined with empty name",
		},
		{
			name: "default subset does not exist",
			entry: &ServiceResolverConfigEntry{
				Kind:          ServiceResolver,
				Name:          "test",
				DefaultSubset: "gone",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
				},
			},
			validateErr: `DefaultSubset "gone" is not a valid subset`,
		},
		{
			name: "default subset does exist",
			entry: &ServiceResolverConfigEntry{
				Kind:          ServiceResolver,
				Name:          "test",
				DefaultSubset: "v1",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
				},
			},
		},
		{
			name: "empty redirect",
			entry: &ServiceResolverConfigEntry{
				Kind:     ServiceResolver,
				Name:     "test",
				Redirect: &ServiceResolverRedirect{},
			},
			validateErr: "Redirect is empty",
		},
		{
			name: "redirect subset with no service",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Redirect: &ServiceResolverRedirect{
					ServiceSubset: "next",
				},
			},
			validateErr: "Redirect.ServiceSubset defined without Redirect.Service",
		},
		{
			name: "redirect namespace with no service",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Redirect: &ServiceResolverRedirect{
					Namespace: "alternate",
				},
			},
			validateErr: "Redirect.Namespace defined without Redirect.Service",
		},
		{
			name: "self redirect with invalid subset",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Redirect: &ServiceResolverRedirect{
					Service:       "test",
					ServiceSubset: "gone",
				},
			},
			validateErr: `Redirect.ServiceSubset "gone" is not a valid subset of "test"`,
		},
		{
			name: "self redirect with valid subset",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Redirect: &ServiceResolverRedirect{
					Service:       "test",
					ServiceSubset: "v1",
				},
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
				},
			},
		},
		{
			name: "simple wildcard failover",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Failover: map[string]ServiceResolverFailover{
					"*": {
						Datacenters: []string{"dc2"},
					},
				},
			},
		},
		{
			name: "failover for missing subset",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Failover: map[string]ServiceResolverFailover{
					"gone": {
						Datacenters: []string{"dc2"},
					},
				},
			},
			validateErr: `Bad Failover["gone"]: not a valid subset`,
		},
		{
			name: "failover for present subset",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
				},
				Failover: map[string]ServiceResolverFailover{
					"v1": {
						Datacenters: []string{"dc2"},
					},
				},
			},
		},
		{
			name: "failover empty",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
				},
				Failover: map[string]ServiceResolverFailover{
					"v1": {},
				},
			},
			validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, Namespace, or Datacenters is required`,
		},
		{
			name: "failover to self using invalid subset",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
				},
				Failover: map[string]ServiceResolverFailover{
					"v1": {
						Service:       "test",
						ServiceSubset: "gone",
					},
				},
			},
			validateErr: `Bad Failover["v1"].ServiceSubset "gone" is not a valid subset of "test"`,
		},
		{
			name: "failover to self using valid subset",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {Filter: "Service.Meta.version == v1"},
					"v2": {Filter: "Service.Meta.version == v2"},
				},
				Failover: map[string]ServiceResolverFailover{
					"v1": {
						Service:       "test",
						ServiceSubset: "v2",
					},
				},
			},
		},
		{
			name: "failover with empty datacenters in list",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Failover: map[string]ServiceResolverFailover{
					"*": {
						Service:     "backup",
						Datacenters: []string{"", "dc2", "dc3"},
					},
				},
			},
			validateErr: `Bad Failover["*"].Datacenters: found empty datacenter`,
		},
		{
			name: "bad connect timeout",
			entry: &ServiceResolverConfigEntry{
				Kind:           ServiceResolver,
				Name:           "test",
				ConnectTimeout: -1 * time.Second,
			},
			validateErr: "Bad ConnectTimeout",
		},
	}

	// Bulk add a bunch of similar validation cases.
	for _, invalidSubset := range invalidSubsetNames {
		tc := testcase{
			name: "invalid subset name: " + invalidSubset,
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					invalidSubset: {OnlyPassing: true},
				},
			},
			validateErr: fmt.Sprintf("Subset %q is invalid", invalidSubset),
		}
		cases = append(cases, tc)
	}

	for _, goodSubset := range validSubsetNames {
		tc := testcase{
			name: "valid subset name: " + goodSubset,
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				Subsets: map[string]ServiceResolverSubset{
					goodSubset: {OnlyPassing: true},
				},
			},
		}
		cases = append(cases, tc)
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			err := tc.entry.Normalize()
			if tc.normalizeErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.normalizeErr)
				return
			}
			require.NoError(t, err)

			if tc.check != nil {
				tc.check(t, tc.entry)
			}

			err = tc.entry.Validate()
			if tc.validateErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.validateErr)
				return
			}
			require.NoError(t, err)
		})
	}
}

func TestServiceResolverConfigEntry_LoadBalancer(t *testing.T) {

	type testcase struct {
		name         string
		entry        *ServiceResolverConfigEntry
		normalizeErr string
		validateErr  string

		// check is called between normalize and validate
		check func(t *testing.T, entry *ServiceResolverConfigEntry)
	}

	cases := []testcase{
		{
			name: "empty policy is valid",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: "",
				},
			},
		},
		{
			name: "supported policy",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyRandom,
				},
			},
		},
		{
			name: "unsupported policy",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: "fake-policy",
				},
			},
			validateErr: `"fake-policy" is not supported`,
		},
		{
			name: "bad policy for least request config",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy:             LBPolicyRingHash,
					LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10},
				},
			},
			validateErr: `LeastRequestConfig specified for incompatible load balancing policy`,
		},
		{
			name: "bad policy for ring hash config",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy:         LBPolicyLeastRequest,
					RingHashConfig: &RingHashConfig{MinimumRingSize: 1024},
				},
			},
			validateErr: `RingHashConfig specified for incompatible load balancing policy`,
		},
		{
			name: "good policy for ring hash config",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy:         LBPolicyRingHash,
					RingHashConfig: &RingHashConfig{MinimumRingSize: 1024},
				},
			},
		},
		{
			name: "good policy for least request config",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy:             LBPolicyLeastRequest,
					LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 2},
				},
			},
		},
		{
			name: "empty policy is not defaulted",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: "",
				},
			},
			check: func(t *testing.T, entry *ServiceResolverConfigEntry) {
				require.Equal(t, "", entry.LoadBalancer.Policy)
			},
		},
		{
			name: "empty policy with hash policy",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: "",
					HashPolicies: []HashPolicy{
						{
							SourceIP: true,
						},
					},
				},
			},
			validateErr: `HashPolicies specified for non-hash-based Policy`,
		},
		{
			name: "cookie config with header policy",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field:      HashPolicyHeader,
							FieldValue: "x-user-id",
							CookieConfig: &CookieConfig{
								TTL:  10 * time.Second,
								Path: "/root",
							},
						},
					},
				},
			},
			validateErr: `cookie_config provided for "header"`,
		},
		{
			name: "cannot generate session cookie with ttl",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field:      HashPolicyCookie,
							FieldValue: "good-cookie",
							CookieConfig: &CookieConfig{
								Session: true,
								TTL:     10 * time.Second,
							},
						},
					},
				},
			},
			validateErr: `a session cookie cannot have an associated TTL`,
		},
		{
			name: "valid cookie policy",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field:      HashPolicyCookie,
							FieldValue: "good-cookie",
							CookieConfig: &CookieConfig{
								TTL:  10 * time.Second,
								Path: "/oven",
							},
						},
					},
				},
			},
		},
		{
			name: "supported match field",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field:      "header",
							FieldValue: "X-Consul-Token",
						},
					},
				},
			},
		},
		{
			name: "unsupported match field",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field: "fake-field",
						},
					},
				},
			},
			validateErr: `"fake-field" is not a supported field`,
		},
		{
			name: "cannot match on source address and custom field",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field:    "header",
							SourceIP: true,
						},
					},
				},
			},
			validateErr: `A single hash policy cannot hash both a source address and a "header"`,
		},
		{
			name: "matchvalue not compatible with source address",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							FieldValue: "X-Consul-Token",
							SourceIP:   true,
						},
					},
				},
			},
			validateErr: `A FieldValue cannot be specified when hashing SourceIP`,
		},
		{
			name: "field without match value",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							Field: "header",
						},
					},
				},
			},
			validateErr: `Field "header" was specified without a FieldValue`,
		},
		{
			name: "field without match value",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy: LBPolicyMaglev,
					HashPolicies: []HashPolicy{
						{
							FieldValue: "my-cookie",
						},
					},
				},
			},
			validateErr: `FieldValue requires a Field to apply to`,
		},
		{
			name: "ring hash kitchen sink",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy:         LBPolicyRingHash,
					RingHashConfig: &RingHashConfig{MaximumRingSize: 10, MinimumRingSize: 2},
					HashPolicies: []HashPolicy{
						{
							Field:      "cookie",
							FieldValue: "my-cookie",
						},
						{
							Field:      "header",
							FieldValue: "alt-header",
							Terminal:   true,
						},
					},
				},
			},
		},
		{
			name: "least request kitchen sink",
			entry: &ServiceResolverConfigEntry{
				Kind: ServiceResolver,
				Name: "test",
				LoadBalancer: &LoadBalancer{
					Policy:             LBPolicyLeastRequest,
					LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 20},
				},
			},
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			err := tc.entry.Normalize()
			if tc.normalizeErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.normalizeErr)
				return
			}
			require.NoError(t, err)

			if tc.check != nil {
				tc.check(t, tc.entry)
			}

			err = tc.entry.Validate()
			if tc.validateErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.validateErr)
				return
			}
			require.NoError(t, err)
		})
	}
}

func TestServiceSplitterConfigEntry(t *testing.T) {

	makesplitter := func(splits ...ServiceSplit) *ServiceSplitterConfigEntry {
		return &ServiceSplitterConfigEntry{
			Kind:   ServiceSplitter,
			Name:   "test",
			Splits: splits,
		}
	}

	makesplit := func(weight float32, service, serviceSubset, namespace string) ServiceSplit {
		return ServiceSplit{
			Weight:        weight,
			Service:       service,
			ServiceSubset: serviceSubset,
			Namespace:     namespace,
		}
	}

	for _, tc := range []struct {
		name         string
		entry        *ServiceSplitterConfigEntry
		normalizeErr string
		validateErr  string
		// check is called between normalize and validate
		check func(t *testing.T, entry *ServiceSplitterConfigEntry)
	}{
		{
			name:         "nil",
			entry:        nil,
			normalizeErr: "config entry is nil",
		},
		{
			name:        "no name",
			entry:       &ServiceSplitterConfigEntry{},
			validateErr: "Name is required",
		},
		{
			name:        "empty",
			entry:       makesplitter(),
			validateErr: "no splits configured",
		},
		{
			name: "1 split",
			entry: makesplitter(
				makesplit(100, "test", "", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(100), entry.Splits[0].Weight)
			},
		},
		{
			name: "1 split not enough weight",
			entry: makesplitter(
				makesplit(99.99, "test", "", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(99.99), entry.Splits[0].Weight)
			},
			validateErr: "the sum of all split weights must be 100",
		},
		{
			name: "1 split too much weight",
			entry: makesplitter(
				makesplit(100.01, "test", "", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(100.01), entry.Splits[0].Weight)
			},
			validateErr: "the sum of all split weights must be 100",
		},
		{
			name: "2 splits",
			entry: makesplitter(
				makesplit(99, "test", "v1", ""),
				makesplit(1, "test", "v2", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(99), entry.Splits[0].Weight)
				require.Equal(t, float32(1), entry.Splits[1].Weight)
			},
		},
		{
			name: "2 splits - rounded up to smallest units",
			entry: makesplitter(
				makesplit(99.999, "test", "v1", ""),
				makesplit(0.001, "test", "v2", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(100), entry.Splits[0].Weight)
				require.Equal(t, float32(0), entry.Splits[1].Weight)
			},
		},
		{
			name: "2 splits not enough weight",
			entry: makesplitter(
				makesplit(99.98, "test", "v1", ""),
				makesplit(0.01, "test", "v2", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(99.98), entry.Splits[0].Weight)
				require.Equal(t, float32(0.01), entry.Splits[1].Weight)
			},
			validateErr: "the sum of all split weights must be 100",
		},
		{
			name: "2 splits too much weight",
			entry: makesplitter(
				makesplit(100, "test", "v1", ""),
				makesplit(0.01, "test", "v2", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(100), entry.Splits[0].Weight)
				require.Equal(t, float32(0.01), entry.Splits[1].Weight)
			},
			validateErr: "the sum of all split weights must be 100",
		},
		{
			name: "3 splits",
			entry: makesplitter(
				makesplit(34, "test", "v1", ""),
				makesplit(33, "test", "v2", ""),
				makesplit(33, "test", "v3", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(34), entry.Splits[0].Weight)
				require.Equal(t, float32(33), entry.Splits[1].Weight)
				require.Equal(t, float32(33), entry.Splits[2].Weight)
			},
		},
		{
			name: "3 splits one duplicated same weights",
			entry: makesplitter(
				makesplit(34, "test", "v1", ""),
				makesplit(33, "test", "v2", ""),
				makesplit(33, "test", "v2", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(34), entry.Splits[0].Weight)
				require.Equal(t, float32(33), entry.Splits[1].Weight)
				require.Equal(t, float32(33), entry.Splits[2].Weight)
			},
			validateErr: "split destination occurs more than once",
		},
		{
			name: "3 splits one duplicated diff weights",
			entry: makesplitter(
				makesplit(34, "test", "v1", ""),
				makesplit(33, "test", "v2", ""),
				makesplit(33, "test", "v1", ""),
			),
			check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
				require.Equal(t, float32(34), entry.Splits[0].Weight)
				require.Equal(t, float32(33), entry.Splits[1].Weight)
				require.Equal(t, float32(33), entry.Splits[2].Weight)
			},
			validateErr: "split destination occurs more than once",
		},
	} {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			err := tc.entry.Normalize()
			if tc.normalizeErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.normalizeErr)
				return
			}
			require.NoError(t, err)

			if tc.check != nil {
				tc.check(t, tc.entry)
			}

			err = tc.entry.Validate()
			if tc.validateErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.validateErr)
				return
			}
			require.NoError(t, err)
		})
	}
}

func TestServiceRouterConfigEntry(t *testing.T) {

	httpMatch := func(http *ServiceRouteHTTPMatch) *ServiceRouteMatch {
		return &ServiceRouteMatch{HTTP: http}
	}
	httpMatchHeader := func(headers ...ServiceRouteHTTPMatchHeader) *ServiceRouteMatch {
		return httpMatch(&ServiceRouteHTTPMatch{
			Header: headers,
		})
	}
	httpMatchParam := func(params ...ServiceRouteHTTPMatchQueryParam) *ServiceRouteMatch {
		return httpMatch(&ServiceRouteHTTPMatch{
			QueryParam: params,
		})
	}
	toService := func(svc string) *ServiceRouteDestination {
		return &ServiceRouteDestination{Service: svc}
	}
	routeMatch := func(match *ServiceRouteMatch) ServiceRoute {
		return ServiceRoute{
			Match:       match,
			Destination: toService("other"),
		}
	}
	makerouter := func(routes ...ServiceRoute) *ServiceRouterConfigEntry {
		return &ServiceRouterConfigEntry{
			Kind:   ServiceRouter,
			Name:   "test",
			Routes: routes,
		}
	}

	type testcase struct {
		name         string
		entry        *ServiceRouterConfigEntry
		normalizeErr string
		validateErr  string
		// check is called between normalize and validate
		check func(t *testing.T, entry *ServiceRouterConfigEntry)
	}

	cases := []testcase{
		{
			name:         "nil",
			entry:        nil,
			normalizeErr: "config entry is nil",
		},
		{
			name:        "no name",
			entry:       &ServiceRouterConfigEntry{},
			validateErr: "Name is required",
		},
		{
			name:  "empty",
			entry: makerouter(),
		},
		{
			name: "1 empty route",
			entry: makerouter(
				ServiceRoute{},
			),
		},

		{
			name: "route with path exact",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathExact: "/exact",
			}))),
		},
		{
			name: "route with bad path exact",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathExact: "no-leading-slash",
			}))),
			validateErr: "PathExact doesn't start with '/'",
		},
		{
			name: "route with path prefix",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathPrefix: "/prefix",
			}))),
		},
		{
			name: "route with bad path prefix",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathPrefix: "no-leading-slash",
			}))),
			validateErr: "PathPrefix doesn't start with '/'",
		},
		{
			name: "route with path regex",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathRegex: "/regex",
			}))),
		},
		{
			name: "route with path exact and prefix",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathExact:  "/exact",
				PathPrefix: "/prefix",
			}))),
			validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
		},
		{
			name: "route with path exact and regex",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathExact: "/exact",
				PathRegex: "/regex",
			}))),
			validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
		},
		{
			name: "route with path prefix and regex",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathPrefix: "/prefix",
				PathRegex:  "/regex",
			}))),
			validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
		},
		{
			name: "route with path exact, prefix, and regex",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				PathExact:  "/exact",
				PathPrefix: "/prefix",
				PathRegex:  "/regex",
			}))),
			validateErr: "should only contain at most one of PathExact, PathPrefix, or PathRegex",
		},

		{
			name: "route with no name header",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Present: true,
			}))),
			validateErr: "missing required Name field",
		},
		{
			name: "route with header present",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:    "foo",
				Present: true,
			}))),
		},
		{
			name: "route with header not present",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:    "foo",
				Present: true,
				Invert:  true,
			}))),
		},
		{
			name: "route with header exact",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:  "foo",
				Exact: "bar",
			}))),
		},
		{
			name: "route with header regex",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:  "foo",
				Regex: "bar",
			}))),
		},
		{
			name: "route with header prefix",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:   "foo",
				Prefix: "bar",
			}))),
		},
		{
			name: "route with header suffix",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:   "foo",
				Suffix: "bar",
			}))),
		},
		{
			name: "route with header present and exact",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:    "foo",
				Present: true,
				Exact:   "bar",
			}))),
			validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
		},
		{
			name: "route with header present and regex",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:    "foo",
				Present: true,
				Regex:   "bar",
			}))),
			validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
		},
		{
			name: "route with header present and prefix",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:    "foo",
				Present: true,
				Prefix:  "bar",
			}))),
			validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
		},
		{
			name: "route with header present and suffix",
			entry: makerouter(routeMatch(httpMatchHeader(ServiceRouteHTTPMatchHeader{
				Name:    "foo",
				Present: true,
				Suffix:  "bar",
			}))),
			validateErr: "should only contain one of Present, Exact, Prefix, Suffix, or Regex",
		},
		// NOTE: Some combinatoric cases for header operators (some 5 choose 2,
		// all 5 choose 3, all 5 choose 4, all 5 choose 5) are omitted from
		// testing.

		////////////////
		{
			name: "route with no name query param",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Exact: "foo",
			}))),
			validateErr: "missing required Name field",
		},
		{
			name: "route with query param exact match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:  "foo",
				Exact: "bar",
			}))),
		},
		{
			name: "route with query param regex match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:  "foo",
				Regex: "bar",
			}))),
		},
		{
			name: "route with query param present match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:    "foo",
				Present: true,
			}))),
		},
		{
			name: "route with query param exact and regex match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:  "foo",
				Exact: "bar",
				Regex: "bar",
			}))),
			validateErr: "should only contain one of Present, Exact, or Regex",
		},
		{
			name: "route with query param exact and present match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:    "foo",
				Exact:   "bar",
				Present: true,
			}))),
			validateErr: "should only contain one of Present, Exact, or Regex",
		},
		{
			name: "route with query param regex and present match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:    "foo",
				Regex:   "bar",
				Present: true,
			}))),
			validateErr: "should only contain one of Present, Exact, or Regex",
		},
		{
			name: "route with query param exact, regex, and present match",
			entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{
				Name:    "foo",
				Exact:   "bar",
				Regex:   "bar",
				Present: true,
			}))),
			validateErr: "should only contain one of Present, Exact, or Regex",
		},
		////////////////
		{
			name: "route with no match and prefix rewrite",
			entry: makerouter(ServiceRoute{
				Match: nil,
				Destination: &ServiceRouteDestination{
					Service:       "other",
					PrefixRewrite: "/new",
				},
			}),
			validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
		},
		{
			name: "route with path prefix match and prefix rewrite",
			entry: makerouter(ServiceRoute{
				Match: httpMatch(&ServiceRouteHTTPMatch{
					PathPrefix: "/api",
				}),
				Destination: &ServiceRouteDestination{
					Service:       "other",
					PrefixRewrite: "/new",
				},
			}),
		},
		{
			name: "route with path exact match and prefix rewrite",
			entry: makerouter(ServiceRoute{
				Match: httpMatch(&ServiceRouteHTTPMatch{
					PathExact: "/api",
				}),
				Destination: &ServiceRouteDestination{
					Service:       "other",
					PrefixRewrite: "/new",
				},
			}),
		},
		{
			name: "route with path regex match and prefix rewrite",
			entry: makerouter(ServiceRoute{
				Match: httpMatch(&ServiceRouteHTTPMatch{
					PathRegex: "/api",
				}),
				Destination: &ServiceRouteDestination{
					Service:       "other",
					PrefixRewrite: "/new",
				},
			}),
			validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
		},
		{
			name: "route with header match and prefix rewrite",
			entry: makerouter(ServiceRoute{
				Match: httpMatchHeader(ServiceRouteHTTPMatchHeader{
					Name:  "foo",
					Exact: "bar",
				}),
				Destination: &ServiceRouteDestination{
					Service:       "other",
					PrefixRewrite: "/new",
				},
			}),
			validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
		},
		{
			name: "route with header match and prefix rewrite",
			entry: makerouter(ServiceRoute{
				Match: httpMatchParam(ServiceRouteHTTPMatchQueryParam{
					Name:  "foo",
					Exact: "bar",
				}),
				Destination: &ServiceRouteDestination{
					Service:       "other",
					PrefixRewrite: "/new",
				},
			}),
			validateErr: "cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix",
		},
		////////////////
		{
			name: "route with method matches",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				Methods: []string{
					"get", "POST", "dElEtE",
				},
			}))),
			check: func(t *testing.T, entry *ServiceRouterConfigEntry) {
				m := entry.Routes[0].Match.HTTP.Methods
				require.Equal(t, []string{"GET", "POST", "DELETE"}, m)
			},
		},
		{
			name: "route with method matches repeated",
			entry: makerouter(routeMatch(httpMatch(&ServiceRouteHTTPMatch{
				Methods: []string{
					"GET", "DELETE", "get",
				},
			}))),
			validateErr: "Methods contains \"GET\" more than once",
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			err := tc.entry.Normalize()
			if tc.normalizeErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.normalizeErr)
				return
			}
			require.NoError(t, err)

			if tc.check != nil {
				tc.check(t, tc.entry)
			}

			err = tc.entry.Validate()
			if tc.validateErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.validateErr)
				return
			}
			require.NoError(t, err)
		})
	}
}

var validSubsetNames = []string{
	"a", "aa", "2a", "a2", "a2a", "a22a",
	"1", "11", "10", "01",
	"a-a", "a--a", "a--a--a",
	"0-0", "0--0", "0--0--0",
	strings.Repeat("a", 63),
}

var invalidSubsetNames = []string{
	"A", "AA", "2A", "A2", "A2A", "A22A",
	"A-A", "A--A", "A--A--A",
	" ", " a", "a ", "a a",
	"_", "_a", "a_", "a_a",
	".", ".a", "a.", "a.a",
	"-", "-a", "a-",
	strings.Repeat("a", 64),
}

func TestValidateServiceSubset(t *testing.T) {
	for _, name := range validSubsetNames {
		t.Run(name, func(t *testing.T) {
			require.NoError(t, validateServiceSubset(name))
		})
	}

	for _, name := range invalidSubsetNames {
		t.Run(name, func(t *testing.T) {
			require.Error(t, validateServiceSubset(name))
		})
	}
}

func TestIsProtocolHTTPLike(t *testing.T) {
	assert.False(t, IsProtocolHTTPLike(""))
	assert.False(t, IsProtocolHTTPLike("tcp"))

	assert.True(t, IsProtocolHTTPLike("http"))
	assert.True(t, IsProtocolHTTPLike("http2"))
	assert.True(t, IsProtocolHTTPLike("grpc"))
}
