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

package discoverychain

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/agent/configentry"
	"github.com/hashicorp/consul/agent/connect"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/proto/private/pbcommon"
	"github.com/hashicorp/consul/proto/private/pbpeering"
)

type compileTestCase struct {
	entries        *configentry.DiscoveryChainSet
	setup          func(req *CompileRequest)
	expect         *structs.CompiledDiscoveryChain
	expectCustom   bool
	expectErr      string
	expectGraphErr bool
}

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

	cases := map[string]compileTestCase{
		"router with defaults":                             testcase_JustRouterWithDefaults(),
		"router with defaults and resolver":                testcase_RouterWithDefaults_NoSplit_WithResolver(),
		"router with defaults and noop split":              testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver(),
		"router with defaults and noop split and resolver": testcase_RouterWithDefaults_WithNoopSplit_WithResolver(),
		"router with no destination":                       testcase_JustRouterWithNoDestination(),
		"route bypasses splitter":                          testcase_RouteBypassesSplit(),
		"noop split":                                       testcase_NoopSplit_DefaultResolver(),
		"noop split with protocol from proxy defaults":     testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults(),
		"noop split with resolver":                         testcase_NoopSplit_WithResolver(),
		"subset split":                                     testcase_SubsetSplit(),
		"service split":                                    testcase_ServiceSplit(),
		"split bypasses next splitter":                     testcase_SplitBypassesSplit(),
		"service redirect":                                 testcase_ServiceRedirect(),
		"service and subset redirect":                      testcase_ServiceAndSubsetRedirect(),
		"datacenter redirect":                              testcase_DatacenterRedirect(),
		"redirect to cluster peer":                         testcase_PeerRedirect(),
		"redirect to cluster peer http proxy-defaults":     testcase_PeerRedirectProxyDefHTTP(),
		"redirect to cluster peer http service-defaults":   testcase_PeerRedirectSvcDefHTTP(),
		"datacenter redirect with mesh gateways":           testcase_DatacenterRedirect_WithMeshGateways(),
		"service failover":                                 testcase_ServiceFailover(),
		"service failover through redirect":                testcase_ServiceFailoverThroughRedirect(),
		"circular resolver failover":                       testcase_Resolver_CircularFailover(),
		"service and subset failover":                      testcase_ServiceAndSubsetFailover(),
		"datacenter failover":                              testcase_DatacenterFailover(),
		"datacenter failover with mesh gateways":           testcase_DatacenterFailover_WithMeshGateways(),
		"target failover":                                  testcase_Failover_Targets(),
		"noop split to resolver with default subset":       testcase_NoopSplit_WithDefaultSubset(),
		"resolver with default subset":                     testcase_Resolve_WithDefaultSubset(),
		"default resolver with external sni":               testcase_DefaultResolver_ExternalSNI(),
		"resolver with no entries and inferring defaults":  testcase_DefaultResolver(),
		"default resolver with proxy defaults":             testcase_DefaultResolver_WithProxyDefaults(),
		"loadbalancer splitter and resolver":               testcase_LBSplitterAndResolver(),
		"loadbalancer resolver":                            testcase_LBResolver(),
		"service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(),
		"extensions":                            testcase_Extensions(),
		"service meta projection":               testcase_ServiceMetaProjection(),
		"service meta projection with redirect": testcase_ServiceMetaProjectionWithRedirect(),

		"all the bells and whistles": testcase_AllBellsAndWhistles(),
		"multi dc canary":            testcase_MultiDatacenterCanary(),

		// various errors
		"splitter requires valid protocol":        testcase_SplitterRequiresValidProtocol(),
		"router requires valid protocol":          testcase_RouterRequiresValidProtocol(),
		"split to unsplittable protocol":          testcase_SplitToUnsplittableProtocol(),
		"route to unroutable protocol":            testcase_RouteToUnroutableProtocol(),
		"failover crosses protocols":              testcase_FailoverCrossesProtocols(),
		"redirect crosses protocols":              testcase_RedirectCrossesProtocols(),
		"redirect to missing subset":              testcase_RedirectToMissingSubset(),
		"resolver with failover and external sni": testcase_Resolver_ExternalSNI_FailoverNotAllowed(),
		"resolver with subsets and external sni":  testcase_Resolver_ExternalSNI_SubsetsNotAllowed(),
		"resolver with redirect and external sni": testcase_Resolver_ExternalSNI_RedirectNotAllowed(),

		// overrides
		"resolver with protocol from override":         testcase_ResolverProtocolOverride(),
		"resolver with protocol from override ignored": testcase_ResolverProtocolOverrideIgnored(),
		"router ignored due to protocol override":      testcase_RouterIgnored_ResolverProtocolOverride(),

		// circular references
		"circular resolver redirect": testcase_Resolver_CircularRedirect(),
		"circular split":             testcase_CircularSplit(),

		// tproxy
		"tproxy service defaults only":     testcase_ServiceDefaultsTProxy(),
		"tproxy proxy defaults only":       testcase_ProxyDefaultsTProxy(),
		"tproxy service defaults override": testcase_ServiceDefaultsOverrideTProxy(),
	}

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

			// sanity check entries are normalized and valid
			for _, entry := range tc.entries.Routers {
				require.NoError(t, entry.Normalize())
				require.NoError(t, entry.Validate())
			}
			for _, entry := range tc.entries.Splitters {
				require.NoError(t, entry.Normalize())
				require.NoError(t, entry.Validate())
			}
			for _, entry := range tc.entries.Resolvers {
				require.NoError(t, entry.Normalize())
				require.NoError(t, entry.Validate())
			}

			req := CompileRequest{
				ServiceName:           "main",
				EvaluateInNamespace:   "default",
				EvaluateInPartition:   "default",
				EvaluateInDatacenter:  "dc1",
				EvaluateInTrustDomain: "trustdomain.consul",
				Entries:               tc.entries,
			}
			if tc.setup != nil {
				tc.setup(&req)
			}

			res, err := Compile(req)
			if tc.expectErr != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.expectErr)
				_, ok := err.(*structs.ConfigEntryGraphError)
				if tc.expectGraphErr {
					require.True(t, ok, "%T is not a *ConfigEntryGraphError", err)
				} else {
					require.False(t, ok, "did not expect a *ConfigEntryGraphError here: %v", err)
				}
			} else {
				require.NoError(t, err)

				// Avoid requiring unnecessary test boilerplate and inject these
				// ourselves.
				tc.expect.ServiceName = "main"
				tc.expect.Namespace = "default"
				tc.expect.Partition = "default"
				tc.expect.Datacenter = "dc1"

				if tc.expectCustom {
					require.NotEmpty(t, res.CustomizationHash)
					res.CustomizationHash = ""
				} else {
					require.Empty(t, res.CustomizationHash)
				}

				require.Equal(t, tc.expect, res)
			}
		})
	}
}

func testcase_JustRouterWithDefaults() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_JustRouterWithNoDestination() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
			Routes: []structs.ServiceRoute{
				{
					Match: &structs.ServiceRouteMatch{
						HTTP: &structs.ServiceRouteHTTPMatch{
							PathPrefix: "/",
						},
					},
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: &structs.ServiceRoute{
							Match: &structs.ServiceRouteMatch{
								HTTP: &structs.ServiceRouteHTTPMatch{
									PathPrefix: "/",
								},
							},
						},
						NextNode: "resolver:main.default.default.dc1",
					},
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_RouterWithDefaults_NoSplit_WithResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind:           "service-resolver",
			Name:           "main",
			ConnectTimeout: 33 * time.Second,
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 33 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": targetWithConnectTimeout(
				newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
				33*time.Second,
			),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
		},
	)
	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "splitter:main.default.default",
					},
				},
			},
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight: 100,
						},
						Weight:   100,
						NextNode: "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestCase {
	entries := newEntries()
	setGlobalProxyProtocol(entries, "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
		},
	)
	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "splitter:main.default.default",
					},
				},
			},
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight: 100,
						},
						Weight:   100,
						NextNode: "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc1",
			}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
		},
	)
	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind:           "service-resolver",
			Name:           "main",
			ConnectTimeout: 33 * time.Second,
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "splitter:main.default.default",
					},
				},
			},
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight: 100,
						},
						Weight:   100,
						NextNode: "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 33 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": targetWithConnectTimeout(
				newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
				33*time.Second,
			),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_RouteBypassesSplit() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")
	setServiceProtocol(entries, "other", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
			Routes: []structs.ServiceRoute{
				// route direct subset reference (bypass split)
				newSimpleRoute("other", func(r *structs.ServiceRoute) {
					r.Destination.ServiceSubset = "bypass"
				}),
			},
		},
	)
	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "other",
			Splits: []structs.ServiceSplit{
				{Weight: 100, Service: "ignored"},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "other",
			Subsets: map[string]structs.ServiceResolverSubset{
				"bypass": {
					Filter: "Service.Meta.version == bypass",
				},
			},
		},
	)

	router := entries.GetRouter(structs.NewServiceID("main", nil))

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: &router.Routes[0],
						NextNode:   "resolver:bypass.other.default.default.dc1",
					},
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
			"resolver:bypass.other.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "bypass.other.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "bypass.other.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
			"bypass.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "other",
				ServiceSubset: "bypass",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == bypass",
				}
			}),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_NoopSplit_DefaultResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight: 100,
						},
						Weight:   100,
						NextNode: "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_NoopSplit_WithResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind:           "service-resolver",
			Name:           "main",
			ConnectTimeout: 33 * time.Second,
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight: 100,
						},
						Weight:   100,
						NextNode: "resolver:main.default.default.dc1",
					},
				},
			},
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 33 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": targetWithConnectTimeout(
				newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
				33*time.Second,
			),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_SubsetSplit() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 60, ServiceSubset: "v2"},
				{Weight: 40, ServiceSubset: "v1"},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Subsets: map[string]structs.ServiceResolverSubset{
				"v1": {
					Filter: "Service.Meta.version == 1",
				},
				"v2": {
					Filter: "Service.Meta.version == 2",
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight:        60,
							ServiceSubset: "v2",
						},
						Weight:   60,
						NextNode: "resolver:v2.main.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:        40,
							ServiceSubset: "v1",
						},
						Weight:   40,
						NextNode: "resolver:v1.main.default.default.dc1",
					},
				},
			},
			"resolver:v2.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v2.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v2.main.default.default.dc1",
				},
			},
			"resolver:v1.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v1.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v1.main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{

			"v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v2",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 2",
				}
			}),
			"v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v1",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 1",
				}
			}),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceSplit() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")
	setServiceProtocol(entries, "foo", "http")
	setServiceProtocol(entries, "bar", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 60, Service: "foo"},
				{Weight: 40, Service: "bar"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight:  60,
							Service: "foo",
						},
						Weight:   60,
						NextNode: "resolver:foo.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:  40,
							Service: "bar",
						},
						Weight:   40,
						NextNode: "resolver:bar.default.default.dc1",
					},
				},
			},
			"resolver:foo.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "foo.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "foo.default.default.dc1",
				},
			},
			"resolver:bar.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "bar.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "bar.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil),
			"bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_SplitBypassesSplit() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")
	setServiceProtocol(entries, "next", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{
					Weight:        100,
					Service:       "next",
					ServiceSubset: "bypassed",
				},
			},
		},
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "next",
			Splits: []structs.ServiceSplit{
				{
					Weight:        100,
					ServiceSubset: "not-bypassed",
				},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "next",
			Subsets: map[string]structs.ServiceResolverSubset{
				"bypassed": {
					Filter: "Service.Meta.version == bypass",
				},
				"not-bypassed": {
					Filter: "Service.Meta.version != bypass",
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight:        100,
							Service:       "next",
							ServiceSubset: "bypassed",
						},
						Weight:   100,
						NextNode: "resolver:bypassed.next.default.default.dc1",
					},
				},
			},
			"resolver:bypassed.next.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "bypassed.next.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "bypassed.next.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"bypassed.next.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "next",
				ServiceSubset: "bypassed",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == bypass",
				}
			}),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceRedirect() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:other.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:other.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "other.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "other.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceAndSubsetRedirect() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service:       "other",
				ServiceSubset: "v2",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "other",
			Subsets: map[string]structs.ServiceResolverSubset{
				"v1": {
					Filter: "Service.Meta.version == 1",
				},
				"v2": {
					Filter: "Service.Meta.version == 2",
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:v2.other.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:v2.other.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v2.other.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v2.other.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"v2.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "other",
				ServiceSubset: "v2",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 2",
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DatacenterRedirect() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Datacenter: "dc9",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc9",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc9": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc9",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc9",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc9",
			}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_PeerRedirect() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
				Peer:    "cluster-01",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:other.default.default.external.cluster-01",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:other.default.default.external.cluster-01": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "other.default.default.external.cluster-01",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "other.default.default.external.cluster-01",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{
				Service: "other",
				Peer:    "cluster-01",
			}, func(t *structs.DiscoveryTarget) {
				t.SNI = ""
				t.Name = ""
				t.Datacenter = ""
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_PeerRedirectProxyDefHTTP() compileTestCase {
	entries := newEntries()
	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind:     structs.ProxyDefaults,
		Name:     structs.ProxyConfigGlobal,
		Protocol: "http",
		Config: map[string]interface{}{
			"Protocol": "http",
		},
	})
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
				Peer:    "cluster-01",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "resolver:other.default.default.external.cluster-01",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:other.default.default.external.cluster-01": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "other.default.default.external.cluster-01",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "other.default.default.external.cluster-01",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{
				Service: "other",
				Peer:    "cluster-01",
			}, func(t *structs.DiscoveryTarget) {
				t.SNI = ""
				t.Name = ""
				t.Datacenter = ""
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_PeerRedirectSvcDefHTTP() compileTestCase {
	entries := newEntries()
	entries.AddServices(
		&structs.ServiceConfigEntry{
			Kind:     structs.ServiceDefaults,
			Name:     "main",
			Protocol: "http",
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
				Peer:    "cluster-01",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "resolver:other.default.default.external.cluster-01",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:other.default.default.external.cluster-01": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "other.default.default.external.cluster-01",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "other.default.default.external.cluster-01",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{
				Service: "other",
				Peer:    "cluster-01",
			}, func(t *structs.DiscoveryTarget) {
				t.SNI = ""
				t.Name = ""
				t.Datacenter = ""
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DatacenterRedirect_WithMeshGateways() compileTestCase {
	entries := newEntries()
	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind: structs.ProxyDefaults,
		Name: structs.ProxyConfigGlobal,
		MeshGateway: structs.MeshGatewayConfig{
			Mode: structs.MeshGatewayModeRemote,
		},
	})

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Datacenter: "dc9",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc9",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc9": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc9",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc9",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc9",
			}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceFailover() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {Service: "backup"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{"backup.default.default.dc1"},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1":   newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
			"backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceFailoverThroughRedirect() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "backup",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "actual",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {Service: "backup"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{"actual.default.default.dc1"},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1":   newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
			"actual.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "actual"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_Resolver_CircularFailover() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "backup",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {Service: "main"},
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {Service: "backup"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{"backup.default.default.dc1"},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1":   newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
			"backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceAndSubsetFailover() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Subsets: map[string]structs.ServiceResolverSubset{
				"backup": {
					Filter: "Service.Meta.version == backup",
				},
			},
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {ServiceSubset: "backup"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{"backup.main.default.default.dc1"},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
			"backup.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "backup",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == backup",
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DatacenterFailover() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {Datacenters: []string{"dc2", "dc4"}},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{"main.default.default.dc2", "main.default.default.dc4"},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
			"main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc2",
			}, nil),
			"main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc4",
			}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DatacenterFailover_WithMeshGateways() compileTestCase {
	entries := newEntries()

	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind: structs.ProxyDefaults,
		Name: structs.ProxyConfigGlobal,
		MeshGateway: structs.MeshGatewayConfig{
			Mode: structs.MeshGatewayModeRemote,
		},
	})

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {Datacenters: []string{"dc2", "dc4"}},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{
							"main.default.default.dc2",
							"main.default.default.dc4",
						},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
			"main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc2",
			}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
			"main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc4",
			}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_Failover_Targets() compileTestCase {
	entries := newEntries()

	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind: structs.ProxyDefaults,
		Name: structs.ProxyConfigGlobal,
		MeshGateway: structs.MeshGatewayConfig{
			Mode: structs.MeshGatewayModeRemote,
		},
	})

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {
					Targets: []structs.ServiceResolverFailoverTarget{
						{Datacenter: "dc3"},
						{Service: "new-main"},
						{Peer: "cluster-01"},
						{Peer: "cluster-02"},
					},
				},
			},
		},
	)

	entries.AddPeers(
		&pbpeering.Peering{
			Name: "cluster-01",
			Remote: &pbpeering.RemoteInfo{
				Locality: &pbcommon.Locality{
					Region: "us-west-1",
					Zone:   "us-west-1a",
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
					Failover: &structs.DiscoveryFailover{
						Targets: []string{
							"main.default.default.dc3",
							"new-main.default.default.dc1",
							"main.default.default.external.cluster-01",
							"main.default.default.external.cluster-02",
						},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
			"main.default.default.dc3": newTarget(structs.DiscoveryTargetOpts{
				Service:    "main",
				Datacenter: "dc3",
			}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
			"new-main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "new-main"}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
			"main.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{
				Service: "main",
				Peer:    "cluster-01",
			}, func(t *structs.DiscoveryTarget) {
				t.SNI = ""
				t.Name = ""
				t.Datacenter = ""
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
				t.Locality = &structs.Locality{
					Region: "us-west-1",
					Zone:   "us-west-1a",
				}
			}),
			"main.default.default.external.cluster-02": newTarget(structs.DiscoveryTargetOpts{
				Service: "main",
				Peer:    "cluster-02",
			}, func(t *structs.DiscoveryTarget) {
				t.SNI = ""
				t.Name = ""
				t.Datacenter = ""
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_NoopSplit_WithDefaultSubset() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind:          "service-resolver",
			Name:          "main",
			DefaultSubset: "v2",
			Subsets: map[string]structs.ServiceResolverSubset{
				"v1": {Filter: "Service.Meta.version == 1"},
				"v2": {Filter: "Service.Meta.version == 2"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight: 100,
						},
						Weight:   100,
						NextNode: "resolver:v2.main.default.default.dc1",
					},
				},
			},
			"resolver:v2.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v2.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v2.main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v2",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 2",
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DefaultResolver() compileTestCase {
	entries := newEntries()

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			// TODO-TARGET
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DefaultResolver_WithProxyDefaults() compileTestCase {
	entries := newEntries()

	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind:     structs.ProxyDefaults,
		Name:     structs.ProxyConfigGlobal,
		Protocol: "grpc",
		Config: map[string]interface{}{
			"protocol": "grpc",
		},
		MeshGateway: structs.MeshGatewayConfig{
			Mode: structs.MeshGatewayModeRemote,
		},
	})

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "grpc",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.MeshGateway = structs.MeshGatewayConfig{
					Mode: structs.MeshGatewayModeRemote,
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_Extensions() compileTestCase {
	entries := newEntries()
	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind: structs.ProxyDefaults,
		Name: structs.ProxyConfigGlobal,
		EnvoyExtensions: []structs.EnvoyExtension{
			{
				Name: "ext1",
			},
		},
	})
	entries.AddServices(
		&structs.ServiceConfigEntry{
			Kind: structs.ServiceDefaults,
			Name: "main",
			EnvoyExtensions: []structs.EnvoyExtension{
				{
					Name: "ext2",
				},
			},
		},
	)
	expect := &structs.CompiledDiscoveryChain{
		Protocol: "tcp",
		Default:  true,
		EnvoyExtensions: []structs.EnvoyExtension{
			{
				Name: "ext1",
			},
			{
				Name: "ext2",
			},
		},
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceMetaProjection() compileTestCase {
	entries := newEntries()
	entries.AddServices(
		&structs.ServiceConfigEntry{
			Kind: structs.ServiceDefaults,
			Name: "main",
			Meta: map[string]string{
				"foo": "bar",
				"abc": "123",
			},
		},
	)
	expect := &structs.CompiledDiscoveryChain{
		Protocol: "tcp",
		Default:  true,
		ServiceMeta: map[string]string{
			"foo": "bar",
			"abc": "123",
		},
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceMetaProjectionWithRedirect() compileTestCase {
	entries := newEntries()
	entries.AddServices(
		&structs.ServiceConfigEntry{
			Kind: structs.ServiceDefaults,
			Name: "main",
			Meta: map[string]string{
				"foo": "bar",
				"abc": "123",
			},
		},
		&structs.ServiceConfigEntry{
			Kind: structs.ServiceDefaults,
			Name: "other",
			Meta: map[string]string{
				"zim": "gir",
				"abc": "456",
				"xyz": "999",
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: structs.ServiceResolver,
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol: "tcp",
		ServiceMeta: map[string]string{
			// Note this is main's meta, not other's.
			"foo": "bar",
			"abc": "123",
		},
		StartNode: "resolver:other.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:other.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "other.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "other.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_RedirectToDefaultResolverIsNotDefaultChain() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: structs.ServiceResolver,
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:other.default.default.dc1",
		Default:   false, /*being explicit here because this is the whole point of this test*/
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:other.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "other.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "other.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_Resolve_WithDefaultSubset() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind:          "service-resolver",
			Name:          "main",
			DefaultSubset: "v2",
			Subsets: map[string]structs.ServiceResolverSubset{
				"v1": {Filter: "Service.Meta.version == 1"},
				"v2": {Filter: "Service.Meta.version == 2"},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:v2.main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:v2.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v2.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v2.main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v2",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 2",
				}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_DefaultResolver_ExternalSNI() compileTestCase {
	entries := newEntries()
	entries.AddServices(&structs.ServiceConfigEntry{
		Kind:        structs.ServiceDefaults,
		Name:        "main",
		ExternalSNI: "main.some.other.service.mesh",
	})

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.SNI = "main.some.other.service.mesh"
				t.External = true
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_Resolver_ExternalSNI_FailoverNotAllowed() compileTestCase {
	entries := newEntries()
	entries.AddServices(&structs.ServiceConfigEntry{
		Kind:        structs.ServiceDefaults,
		Name:        "main",
		ExternalSNI: "main.some.other.service.mesh",
	})
	entries.AddResolvers(&structs.ServiceResolverConfigEntry{
		Kind:           "service-resolver",
		Name:           "main",
		ConnectTimeout: 33 * time.Second,
		Failover: map[string]structs.ServiceResolverFailover{
			"*": {Service: "backup"},
		},
	})

	return compileTestCase{
		entries:        entries,
		expectErr:      `service "main" has an external SNI set; cannot define failover for external services`,
		expectGraphErr: true,
	}
}

func testcase_Resolver_ExternalSNI_SubsetsNotAllowed() compileTestCase {
	entries := newEntries()
	entries.AddServices(&structs.ServiceConfigEntry{
		Kind:        structs.ServiceDefaults,
		Name:        "main",
		ExternalSNI: "main.some.other.service.mesh",
	})
	entries.AddResolvers(&structs.ServiceResolverConfigEntry{
		Kind:           "service-resolver",
		Name:           "main",
		ConnectTimeout: 33 * time.Second,
		Subsets: map[string]structs.ServiceResolverSubset{
			"canary": {
				Filter: "Service.Meta.version == canary",
			},
		},
	})

	return compileTestCase{
		entries:        entries,
		expectErr:      `service "main" has an external SNI set; cannot define subsets for external services`,
		expectGraphErr: true,
	}
}

func testcase_Resolver_ExternalSNI_RedirectNotAllowed() compileTestCase {
	entries := newEntries()
	entries.AddServices(&structs.ServiceConfigEntry{
		Kind:        structs.ServiceDefaults,
		Name:        "main",
		ExternalSNI: "main.some.other.service.mesh",
	})
	entries.AddResolvers(&structs.ServiceResolverConfigEntry{
		Kind:           "service-resolver",
		Name:           "main",
		ConnectTimeout: 33 * time.Second,
		Redirect: &structs.ServiceResolverRedirect{
			Datacenter: "dc2",
		},
	})

	return compileTestCase{
		entries:        entries,
		expectErr:      `service "main" has an external SNI set; cannot define redirects for external services`,
		expectGraphErr: true,
	}
}

func testcase_MultiDatacenterCanary() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")
	setServiceProtocol(entries, "main-dc2", "http")
	setServiceProtocol(entries, "main-dc3", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 60, Service: "main-dc2"},
				{Weight: 40, Service: "main-dc3"},
			},
		},
	)
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main-dc2",
			Redirect: &structs.ServiceResolverRedirect{
				Service:    "main",
				Datacenter: "dc2",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main-dc3",
			Redirect: &structs.ServiceResolverRedirect{
				Service:    "main",
				Datacenter: "dc3",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind:           "service-resolver",
			Name:           "main",
			ConnectTimeout: 33 * time.Second,
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight:  60,
							Service: "main-dc2",
						},
						Weight:   60,
						NextNode: "resolver:main.default.default.dc2",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:  40,
							Service: "main-dc3",
						},
						Weight:   40,
						NextNode: "resolver:main.default.default.dc3",
					},
				},
			},
			"resolver:main.default.default.dc2": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc2",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 33 * time.Second,
					Target:         "main.default.default.dc2",
				},
			},
			"resolver:main.default.default.dc3": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc3",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 33 * time.Second,
					Target:         "main.default.default.dc3",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc2": targetWithConnectTimeout(
				newTarget(structs.DiscoveryTargetOpts{
					Service:    "main",
					Datacenter: "dc2",
				}, nil),
				33*time.Second,
			),
			"main.default.default.dc3": targetWithConnectTimeout(
				newTarget(structs.DiscoveryTargetOpts{
					Service:    "main",
					Datacenter: "dc3",
				}, nil),
				33*time.Second,
			),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_AllBellsAndWhistles() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")
	setServiceProtocol(entries, "svc-redirect", "http")
	setServiceProtocol(entries, "svc-redirect-again", "http")
	setServiceProtocol(entries, "svc-split", "http")
	setServiceProtocol(entries, "svc-split-again", "http")
	setServiceProtocol(entries, "svc-split-one-more-time", "http")
	setServiceProtocol(entries, "redirected", "http")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
			Routes: []structs.ServiceRoute{
				newSimpleRoute("svc-redirect"), // double redirected to a default subset
				newSimpleRoute("svc-split"),    // one split is split further
			},
		},
	)
	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "svc-split",
			Splits: []structs.ServiceSplit{
				{Weight: 60, Service: "svc-redirect"},    // double redirected to a default subset
				{Weight: 40, Service: "svc-split-again"}, // split again
			},
		},
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "svc-split-again",
			Splits: []structs.ServiceSplit{
				{Weight: 75, Service: "main", ServiceSubset: "v1"},
				{
					Weight:  25,
					Service: "svc-split-one-more-time",
					RequestHeaders: &structs.HTTPHeaderModifiers{
						Set: map[string]string{
							"parent": "1",
							"shared": "from-parent",
						},
					},
					ResponseHeaders: &structs.HTTPHeaderModifiers{
						Set: map[string]string{
							"parent": "2",
							"shared": "from-parent",
						},
					},
				},
			},
		},
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "svc-split-one-more-time",
			Splits: []structs.ServiceSplit{
				{Weight: 80, Service: "main", ServiceSubset: "v2"},
				{
					Weight:        20,
					Service:       "main",
					ServiceSubset: "v3",
					RequestHeaders: &structs.HTTPHeaderModifiers{
						Set: map[string]string{
							"child":  "3",
							"shared": "from-child",
						},
					},
					ResponseHeaders: &structs.HTTPHeaderModifiers{
						Set: map[string]string{
							"child":  "4",
							"shared": "from-parent",
						},
					},
				},
			},
		},
	)

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "svc-redirect",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "svc-redirect-again",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "svc-redirect-again",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "redirected",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind:          "service-resolver",
			Name:          "redirected",
			DefaultSubset: "prod",
			Subsets: map[string]structs.ServiceResolverSubset{
				"prod": {Filter: "ServiceMeta.env == prod"},
				"qa":   {Filter: "ServiceMeta.env == qa"},
			},
			LoadBalancer: &structs.LoadBalancer{
				Policy: "ring_hash",
				RingHashConfig: &structs.RingHashConfig{
					MaximumRingSize: 100,
				},
				HashPolicies: []structs.HashPolicy{
					{
						SourceIP: true,
					},
				},
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind:          "service-resolver",
			Name:          "main",
			DefaultSubset: "default-subset",
			Subsets: map[string]structs.ServiceResolverSubset{
				"v1":             {Filter: "Service.Meta.version == 1"},
				"v2":             {Filter: "Service.Meta.version == 2"},
				"v3":             {Filter: "Service.Meta.version == 3"},
				"default-subset": {OnlyPassing: true},
			},
		},
	)

	var (
		router = entries.GetRouter(structs.NewServiceID("main", nil))
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "router:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"router:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeRouter,
				Name: "main.default.default",
				Routes: []*structs.DiscoveryRoute{
					{
						Definition: &router.Routes[0],
						NextNode:   "resolver:prod.redirected.default.default.dc1",
					},
					{
						Definition: &router.Routes[1],
						NextNode:   "splitter:svc-split.default.default",
					},
					{
						Definition: newDefaultServiceRoute("main", "default", "default"),
						NextNode:   "resolver:default-subset.main.default.default.dc1",
					},
				},
			},
			"splitter:svc-split.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "svc-split.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight:  60,
							Service: "svc-redirect",
						},
						Weight:   60,
						NextNode: "resolver:prod.redirected.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:        75,
							Service:       "main",
							ServiceSubset: "v1",
						},
						Weight:   30,
						NextNode: "resolver:v1.main.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:        80,
							Service:       "main",
							ServiceSubset: "v2",
							// Should inherit these from parent verbatim as there was no
							// child-split header manip.
							RequestHeaders: &structs.HTTPHeaderModifiers{
								Set: map[string]string{
									"parent": "1",
									"shared": "from-parent",
								},
							},
							ResponseHeaders: &structs.HTTPHeaderModifiers{
								Set: map[string]string{
									"parent": "2",
									"shared": "from-parent",
								},
							},
						},
						Weight:   8,
						NextNode: "resolver:v2.main.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:        20,
							Service:       "main",
							ServiceSubset: "v3",
							// Should get a merge of child and parent rules
							RequestHeaders: &structs.HTTPHeaderModifiers{
								Set: map[string]string{
									"parent": "1",
									"child":  "3",
									"shared": "from-child",
								},
							},
							ResponseHeaders: &structs.HTTPHeaderModifiers{
								Set: map[string]string{
									"parent": "2",
									"child":  "4",
									"shared": "from-parent",
								},
							},
						},
						Weight:   2,
						NextNode: "resolver:v3.main.default.default.dc1",
					},
				},
				LoadBalancer: &structs.LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &structs.RingHashConfig{
						MaximumRingSize: 100,
					},
					HashPolicies: []structs.HashPolicy{
						{
							SourceIP: true,
						},
					},
				},
			},
			"resolver:prod.redirected.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "prod.redirected.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "prod.redirected.default.default.dc1",
				},
				LoadBalancer: &structs.LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &structs.RingHashConfig{
						MaximumRingSize: 100,
					},
					HashPolicies: []structs.HashPolicy{
						{
							SourceIP: true,
						},
					},
				},
			},
			"resolver:v1.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v1.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v1.main.default.default.dc1",
				},
			},
			"resolver:v2.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v2.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v2.main.default.default.dc1",
				},
			},
			"resolver:v3.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "v3.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "v3.main.default.default.dc1",
				},
			},
			"resolver:default-subset.main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "default-subset.main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					ConnectTimeout: 5 * time.Second,
					Target:         "default-subset.main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"prod.redirected.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "redirected",
				ServiceSubset: "prod",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "ServiceMeta.env == prod",
				}
			}),
			"v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v1",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 1",
				}
			}),
			"v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v2",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 2",
				}
			}),
			"v3.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "v3",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{
					Filter: "Service.Meta.version == 3",
				}
			}),
			"default-subset.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{
				Service:       "main",
				ServiceSubset: "default-subset",
			}, func(t *structs.DiscoveryTarget) {
				t.Subset = structs.ServiceResolverSubset{OnlyPassing: true}
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_SplitterRequiresValidProtocol() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "tcp")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: structs.ServiceSplitter,
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 90, Namespace: "v1"},
				{Weight: 10, Namespace: "v2"},
			},
		},
	)

	return compileTestCase{
		entries:        entries,
		expectErr:      "does not permit advanced routing or splitting behavior",
		expectGraphErr: true,
	}
}

func testcase_RouterRequiresValidProtocol() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "tcp")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: structs.ServiceRouter,
			Name: "main",
			Routes: []structs.ServiceRoute{
				{
					Match: &structs.ServiceRouteMatch{
						HTTP: &structs.ServiceRouteHTTPMatch{
							PathExact: "/other",
						},
					},
					Destination: &structs.ServiceRouteDestination{
						Namespace: "other",
					},
				},
			},
		},
	)
	return compileTestCase{
		entries:        entries,
		expectErr:      "does not permit advanced routing or splitting behavior",
		expectGraphErr: true,
	}
}

func testcase_SplitToUnsplittableProtocol() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "tcp")
	setServiceProtocol(entries, "other", "tcp")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: structs.ServiceSplitter,
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 90},
				{Weight: 10, Service: "other"},
			},
		},
	)
	return compileTestCase{
		entries:        entries,
		expectErr:      "does not permit advanced routing or splitting behavior",
		expectGraphErr: true,
	}
}

func testcase_RouteToUnroutableProtocol() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "tcp")
	setServiceProtocol(entries, "other", "tcp")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: structs.ServiceRouter,
			Name: "main",
			Routes: []structs.ServiceRoute{
				{
					Match: &structs.ServiceRouteMatch{
						HTTP: &structs.ServiceRouteHTTPMatch{
							PathExact: "/other",
						},
					},
					Destination: &structs.ServiceRouteDestination{
						Service: "other",
					},
				},
			},
		},
	)

	return compileTestCase{
		entries:        entries,
		expectErr:      "does not permit advanced routing or splitting behavior",
		expectGraphErr: true,
	}
}

func testcase_FailoverCrossesProtocols() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "grpc")
	setServiceProtocol(entries, "other", "tcp")

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: structs.ServiceResolver,
			Name: "main",
			Failover: map[string]structs.ServiceResolverFailover{
				"*": {
					Service: "other",
				},
			},
		},
	)

	return compileTestCase{
		entries:        entries,
		expectErr:      "uses inconsistent protocols",
		expectGraphErr: true,
	}
}

func testcase_RedirectCrossesProtocols() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "grpc")
	setServiceProtocol(entries, "other", "tcp")

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: structs.ServiceResolver,
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
			},
		},
	)
	return compileTestCase{
		entries:        entries,
		expectErr:      "uses inconsistent protocols",
		expectGraphErr: true,
	}
}

func testcase_RedirectToMissingSubset() compileTestCase {
	entries := newEntries()

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind:           structs.ServiceResolver,
			Name:           "other",
			ConnectTimeout: 33 * time.Second,
		},
		&structs.ServiceResolverConfigEntry{
			Kind: structs.ServiceResolver,
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service:       "other",
				ServiceSubset: "v1",
			},
		},
	)

	return compileTestCase{
		entries:        entries,
		expectErr:      `does not have a subset named "v1"`,
		expectGraphErr: true,
	}
}

func testcase_ResolverProtocolOverride() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "grpc")

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http2",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			// TODO-TARGET
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect,
		expectCustom: true,
		setup: func(req *CompileRequest) {
			req.OverrideProtocol = "http2"
		},
	}
}

func testcase_ResolverProtocolOverrideIgnored() compileTestCase {
	// This shows that if you try to override the protocol to its current value
	// the override is completely ignored.
	entries := newEntries()
	setServiceProtocol(entries, "main", "http2")

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http2",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			// TODO-TARGET
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect,
		setup: func(req *CompileRequest) {
			req.OverrideProtocol = "http2"
		},
	}
}

func testcase_RouterIgnored_ResolverProtocolOverride() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "grpc")

	entries.AddRouters(
		&structs.ServiceRouterConfigEntry{
			Kind: "service-router",
			Name: "main",
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		StartNode: "resolver:main.default.default.dc1",
		Default:   true,
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			// TODO-TARGET
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}
	return compileTestCase{entries: entries, expect: expect,
		expectCustom: true,
		setup: func(req *CompileRequest) {
			req.OverrideProtocol = "tcp"
		},
	}
}

func testcase_Resolver_CircularRedirect() compileTestCase {
	entries := newEntries()
	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "other",
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "other",
			Redirect: &structs.ServiceResolverRedirect{
				Service: "main",
			},
		},
	)

	return compileTestCase{entries: entries,
		expectErr:      "detected circular resolver redirect",
		expectGraphErr: true,
	}
}

func testcase_CircularSplit() compileTestCase {
	entries := newEntries()
	setGlobalProxyProtocol(entries, "http")
	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 100, Service: "other"},
			},
		},
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "other",
			Splits: []structs.ServiceSplit{
				{Weight: 100, Service: "main"},
			},
		},
	)

	return compileTestCase{entries: entries,
		expectErr:      "detected circular reference",
		expectGraphErr: true,
	}
}

func testcase_LBSplitterAndResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "foo", "http")
	setServiceProtocol(entries, "bar", "http")
	setServiceProtocol(entries, "baz", "http")

	entries.AddSplitters(
		&structs.ServiceSplitterConfigEntry{
			Kind: "service-splitter",
			Name: "main",
			Splits: []structs.ServiceSplit{
				{Weight: 60, Service: "foo"},
				{Weight: 20, Service: "bar"},
				{Weight: 20, Service: "baz"},
			},
		},
	)

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "foo",
			LoadBalancer: &structs.LoadBalancer{
				Policy: "least_request",
				LeastRequestConfig: &structs.LeastRequestConfig{
					ChoiceCount: 3,
				},
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "bar",
			LoadBalancer: &structs.LoadBalancer{
				Policy: "ring_hash",
				RingHashConfig: &structs.RingHashConfig{
					MaximumRingSize: 101,
				},
				HashPolicies: []structs.HashPolicy{
					{
						SourceIP: true,
					},
				},
			},
		},
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "baz",
			LoadBalancer: &structs.LoadBalancer{
				Policy: "maglev",
				HashPolicies: []structs.HashPolicy{
					{
						Field:      "cookie",
						FieldValue: "chocolate-chip",
						CookieConfig: &structs.CookieConfig{
							TTL:  2 * time.Minute,
							Path: "/bowl",
						},
						Terminal: true,
					},
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "splitter:main.default.default",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"splitter:main.default.default": {
				Type: structs.DiscoveryGraphNodeTypeSplitter,
				Name: "main.default.default",
				Splits: []*structs.DiscoverySplit{
					{
						Definition: &structs.ServiceSplit{
							Weight:  60,
							Service: "foo",
						},
						Weight:   60,
						NextNode: "resolver:foo.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:  20,
							Service: "bar",
						},
						Weight:   20,
						NextNode: "resolver:bar.default.default.dc1",
					},
					{
						Definition: &structs.ServiceSplit{
							Weight:  20,
							Service: "baz",
						},
						Weight:   20,
						NextNode: "resolver:baz.default.default.dc1",
					},
				},
				// The LB config from bar is attached because splitters only care about hash-based policies,
				// and it's the config from bar not baz because we pick the first one we encounter in the Splits.
				LoadBalancer: &structs.LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &structs.RingHashConfig{
						MaximumRingSize: 101,
					},
					HashPolicies: []structs.HashPolicy{
						{
							SourceIP: true,
						},
					},
				},
			},
			// Each service's LB config is passed down from the service-resolver to the resolver node
			"resolver:foo.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "foo.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        false,
					ConnectTimeout: 5 * time.Second,
					Target:         "foo.default.default.dc1",
				},
				LoadBalancer: &structs.LoadBalancer{
					Policy: "least_request",
					LeastRequestConfig: &structs.LeastRequestConfig{
						ChoiceCount: 3,
					},
				},
			},
			"resolver:bar.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "bar.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        false,
					ConnectTimeout: 5 * time.Second,
					Target:         "bar.default.default.dc1",
				},
				LoadBalancer: &structs.LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &structs.RingHashConfig{
						MaximumRingSize: 101,
					},
					HashPolicies: []structs.HashPolicy{
						{
							SourceIP: true,
						},
					},
				},
			},
			"resolver:baz.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "baz.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        false,
					ConnectTimeout: 5 * time.Second,
					Target:         "baz.default.default.dc1",
				},
				LoadBalancer: &structs.LoadBalancer{
					Policy: "maglev",
					HashPolicies: []structs.HashPolicy{
						{
							Field:      "cookie",
							FieldValue: "chocolate-chip",
							CookieConfig: &structs.CookieConfig{
								TTL:  2 * time.Minute,
								Path: "/bowl",
							},
							Terminal: true,
						},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil),
			"bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil),
			"baz.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "baz"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

// ensure chain with LB cfg in resolver isn't a default chain (!IsDefault)
func testcase_LBResolver() compileTestCase {
	entries := newEntries()
	setServiceProtocol(entries, "main", "http")

	entries.AddResolvers(
		&structs.ServiceResolverConfigEntry{
			Kind: "service-resolver",
			Name: "main",
			LoadBalancer: &structs.LoadBalancer{
				Policy: "ring_hash",
				RingHashConfig: &structs.RingHashConfig{
					MaximumRingSize: 101,
				},
				HashPolicies: []structs.HashPolicy{
					{
						SourceIP: true,
					},
				},
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "http",
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        false,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
				LoadBalancer: &structs.LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &structs.RingHashConfig{
						MaximumRingSize: 101,
					},
					HashPolicies: []structs.HashPolicy{
						{
							SourceIP: true,
						},
					},
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
		},
	}

	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceDefaultsTProxy() compileTestCase {
	entries := newEntries()
	entries.AddServices(
		&structs.ServiceConfigEntry{
			Kind: structs.ServiceDefaults,
			Name: "main",
			TransparentProxy: structs.TransparentProxyConfig{
				DialedDirectly: true,
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.TransparentProxy.DialedDirectly = true
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ProxyDefaultsTProxy() compileTestCase {
	entries := newEntries()
	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind: structs.ProxyDefaults,
		Name: structs.ProxyConfigGlobal,
		TransparentProxy: structs.TransparentProxyConfig{
			DialedDirectly: true,
		},
	})

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.TransparentProxy.DialedDirectly = true
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func testcase_ServiceDefaultsOverrideTProxy() compileTestCase {
	entries := newEntries()
	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind: structs.ProxyDefaults,
		Name: structs.ProxyConfigGlobal,
		TransparentProxy: structs.TransparentProxyConfig{
			DialedDirectly: false,
		},
	})
	entries.AddServices(
		&structs.ServiceConfigEntry{
			Kind: structs.ServiceDefaults,
			Name: "main",
			TransparentProxy: structs.TransparentProxyConfig{
				DialedDirectly: true,
			},
		},
	)

	expect := &structs.CompiledDiscoveryChain{
		Protocol:  "tcp",
		Default:   true,
		StartNode: "resolver:main.default.default.dc1",
		Nodes: map[string]*structs.DiscoveryGraphNode{
			"resolver:main.default.default.dc1": {
				Type: structs.DiscoveryGraphNodeTypeResolver,
				Name: "main.default.default.dc1",
				Resolver: &structs.DiscoveryResolver{
					Default:        true,
					ConnectTimeout: 5 * time.Second,
					Target:         "main.default.default.dc1",
				},
			},
		},
		Targets: map[string]*structs.DiscoveryTarget{
			"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) {
				t.TransparentProxy.DialedDirectly = true
			}),
		},
	}
	return compileTestCase{entries: entries, expect: expect}
}

func newSimpleRoute(name string, muts ...func(*structs.ServiceRoute)) structs.ServiceRoute {
	r := structs.ServiceRoute{
		Match: &structs.ServiceRouteMatch{
			HTTP: &structs.ServiceRouteHTTPMatch{PathPrefix: "/" + name},
		},
		Destination: &structs.ServiceRouteDestination{Service: name},
	}

	for _, mut := range muts {
		mut(&r)
	}

	return r
}

func setGlobalProxyProtocol(entries *configentry.DiscoveryChainSet, protocol string) {
	entries.AddProxyDefaults(&structs.ProxyConfigEntry{
		Kind:     structs.ProxyDefaults,
		Name:     structs.ProxyConfigGlobal,
		Protocol: protocol,
		Config: map[string]interface{}{
			"protocol": protocol,
		},
	})
}

func setServiceProtocol(entries *configentry.DiscoveryChainSet, name, protocol string) {
	entries.AddServices(&structs.ServiceConfigEntry{
		Kind:     structs.ServiceDefaults,
		Name:     name,
		Protocol: protocol,
	})
}

func newEntries() *configentry.DiscoveryChainSet {
	return &configentry.DiscoveryChainSet{
		Routers:   make(map[structs.ServiceID]*structs.ServiceRouterConfigEntry),
		Splitters: make(map[structs.ServiceID]*structs.ServiceSplitterConfigEntry),
		Resolvers: make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry),
	}
}

func newTarget(opts structs.DiscoveryTargetOpts, modFn func(t *structs.DiscoveryTarget)) *structs.DiscoveryTarget {
	if opts.Namespace == "" {
		opts.Namespace = "default"
	}
	if opts.Partition == "" {
		opts.Partition = "default"
	}
	if opts.Datacenter == "" {
		opts.Datacenter = "dc1"
	}
	t := structs.NewDiscoveryTarget(opts)
	t.SNI = connect.TargetSNI(t, "trustdomain.consul")
	t.Name = t.SNI
	t.ConnectTimeout = 5 * time.Second // default
	t.PrioritizeByLocality = opts.PrioritizeByLocality
	if modFn != nil {
		modFn(t)
	}
	return t
}

func targetWithConnectTimeout(t *structs.DiscoveryTarget, connectTimeout time.Duration) *structs.DiscoveryTarget {
	t.ConnectTimeout = connectTimeout
	return t
}
