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

package api

import (
	"reflect"
	"testing"
	"time"

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

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

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

	catalog := c.Catalog()
	retry.Run(t, func(r *retry.R) {
		datacenters, err := catalog.Datacenters()
		if err != nil {
			r.Fatal(err)
		}
		if len(datacenters) < 1 {
			r.Fatal("got 0 datacenters want at least one")
		}
	})
}

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

	s.WaitForSerfCheck(t)
	catalog := c.Catalog()

	retry.Run(t, func(r *retry.R) {
		nodes, meta, err := catalog.Nodes(nil)
		require.NoError(r, err)
		require.Len(r, nodes, 1)
		require.True(r, meta.LastIndex >= 1, "Last index must be greater than 1")

		// The raft indexes are not relevant for this test.
		got := nodes[0]
		got.CreateIndex = 0
		got.ModifyIndex = 0

		want := &Node{
			ID:         s.Config.NodeID,
			Node:       s.Config.NodeName,
			Partition:  defaultPartition,
			Address:    "127.0.0.1",
			Datacenter: "dc1",
			TaggedAddresses: map[string]string{
				"lan":      "127.0.0.1",
				"lan_ipv4": "127.0.0.1",
				"wan":      "127.0.0.1",
				"wan_ipv4": "127.0.0.1",
			},
			Meta: map[string]string{
				"consul-network-segment": "",
				"consul-version":         s.Config.Version,
			},
		}
		require.Equal(r, want, got)
	})
}

func TestAPI_CatalogNodes_MetaFilter(t *testing.T) {
	t.Parallel()
	meta := map[string]string{"somekey": "somevalue"}
	c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.NodeMeta = meta
	})
	defer s.Stop()

	catalog := c.Catalog()
	// Make sure we get the node back when filtering by its metadata
	retry.Run(t, func(r *retry.R) {
		nodes, meta, err := catalog.Nodes(&QueryOptions{NodeMeta: meta})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(nodes) == 0 {
			r.Fatalf("Bad: %v", nodes)
		}

		if _, ok := nodes[0].TaggedAddresses["wan"]; !ok {
			r.Fatalf("Bad: %v", nodes[0])
		}

		if v, ok := nodes[0].Meta["somekey"]; !ok || v != "somevalue" {
			r.Fatalf("Bad: %v", nodes[0].Meta)
		}

		if nodes[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", nodes[0])
		}
	})

	retry.Run(t, func(r *retry.R) {
		// Get nothing back when we use an invalid filter
		nodes, meta, err := catalog.Nodes(&QueryOptions{NodeMeta: map[string]string{"nope": "nope"}})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(nodes) != 0 {
			r.Fatalf("Bad: %v", nodes)
		}
	})
}

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

	// this sets up the catalog entries with things we can filter on
	testNodeServiceCheckRegistrations(t, c, "dc1")

	catalog := c.Catalog()
	nodes, _, err := catalog.Nodes(nil)
	require.NoError(t, err)
	// 3 nodes inserted by the setup func above plus the agent itself
	require.Len(t, nodes, 4)

	// now filter down to just a couple nodes with a specific meta entry
	nodes, _, err = catalog.Nodes(&QueryOptions{Filter: "Meta.env == production"})
	require.NoError(t, err)
	require.Len(t, nodes, 2)

	// filter out everything that isn't bar or baz
	nodes, _, err = catalog.Nodes(&QueryOptions{Filter: "Node == bar or Node == baz"})
	require.NoError(t, err)
	require.Len(t, nodes, 2)

	// check for non-existent ip for the node addr
	nodes, _, err = catalog.Nodes(&QueryOptions{Filter: "Address == `10.0.0.1`"})
	require.NoError(t, err)
	require.Empty(t, nodes)
}

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

	catalog := c.Catalog()
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Services(nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}
	})
}

func TestAPI_CatalogServices_NodeMetaFilter(t *testing.T) {
	t.Parallel()
	meta := map[string]string{"somekey": "somevalue"}
	c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.NodeMeta = meta
	})
	defer s.Stop()

	catalog := c.Catalog()
	// Make sure we get the service back when filtering by the node's metadata
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Services(&QueryOptions{NodeMeta: meta})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}
	})

	retry.Run(t, func(r *retry.R) {
		// Get nothing back when using an invalid filter
		services, meta, err := catalog.Services(&QueryOptions{NodeMeta: map[string]string{"nope": "nope"}})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) != 0 {
			r.Fatalf("Bad: %v", services)
		}
	})
}

func TestAPI_CatalogServices_FilterExpr_NodeMeta(t *testing.T) {
	t.Parallel()
	meta := map[string]string{"somekey": "somevalue", "synthetic": "true"}
	c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.NodeMeta = meta
	})
	defer s.Stop()

	catalog := c.Catalog()
	// Make sure we get the service back when filtering by filter expression
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Services(&QueryOptions{Filter: "NodeMeta[\"synthetic\"] == true and NodeMeta[\"somekey\"] == somevalue"})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}
		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}
	})
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Services(&QueryOptions{Filter: "NodeMeta.synthetic == true"})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}
	})
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Services(&QueryOptions{Filter: "NodeMeta.somekey == somevalue"})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}
	})
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Services(&QueryOptions{Filter: "NodeMeta.nope == nope"})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) != 0 {
			r.Fatalf("Bad: %v", services)
		}
	})
}

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

	catalog := c.Catalog()

	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Service("consul", "", nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}

		if services[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", services[0])
		}
	})
}

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

	catalog := c.Catalog()

	proxyReg := testUnmanagedProxyRegistration(t)

	retry.Run(t, func(r *retry.R) {
		_, err := catalog.Register(proxyReg, nil)
		r.Check(err)

		services, meta, err := catalog.Service("web-proxy", "", nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}

		if services[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", services[0])
		}

		if !reflect.DeepEqual(services[0].ServiceProxy, proxyReg.Service.Proxy) {
			r.Fatalf("bad proxy.\nwant: %v\n got: %v", proxyReg.Service.Proxy,
				services[0].ServiceProxy)
		}
	})
}

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

	catalog := c.Catalog()

	q := &QueryOptions{
		UseCache: true,
	}

	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Service("consul", "", q)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}

		if services[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", services[0])
		}
	})

	// Got success, next hit must be cache hit
	_, meta, err := catalog.Service("consul", "", q)
	require.NoError(t, err)
	require.True(t, meta.CacheHit)
	require.Equal(t, time.Duration(0), meta.CacheAge)
}

func TestAPI_CatalogService_SingleTag(t *testing.T) {
	t.Parallel()
	c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.NodeName = "node123"
	})
	defer s.Stop()

	agent := c.Agent()
	catalog := c.Catalog()
	locality := &Locality{Region: "us-west-1", Zone: "us-west-1a"}
	reg := &AgentServiceRegistration{
		Name:     "foo",
		ID:       "foo1",
		Tags:     []string{"bar"},
		Locality: locality,
	}
	require.NoError(t, agent.ServiceRegister(reg))
	defer agent.ServiceDeregister("foo1")

	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Service("foo", "bar", nil)
		require.NoError(r, err)
		require.NotEqual(r, meta.LastIndex, 0)
		require.Len(r, services, 1)
		require.Equal(r, services[0].ServiceID, "foo1")
		require.Equal(r, locality, services[0].ServiceLocality)
	})
}

func TestAPI_CatalogService_MultipleTags(t *testing.T) {
	t.Parallel()
	c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.NodeName = "node123"
	})
	defer s.Stop()

	agent := c.Agent()
	catalog := c.Catalog()

	// Make two services with a check
	reg := &AgentServiceRegistration{
		Name: "foo",
		ID:   "foo1",
		Tags: []string{"bar"},
	}
	require.NoError(t, agent.ServiceRegister(reg))
	defer agent.ServiceDeregister("foo1")

	reg2 := &AgentServiceRegistration{
		Name: "foo",
		ID:   "foo2",
		Tags: []string{"bar", "v2"},
	}
	require.NoError(t, agent.ServiceRegister(reg2))
	defer agent.ServiceDeregister("foo2")

	// Test searching with one tag (two results)
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.ServiceMultipleTags("foo", []string{"bar"}, nil)

		require.NoError(r, err)
		require.NotEqual(r, meta.LastIndex, 0)

		// Should be 2 services with the `bar` tag
		require.Len(r, services, 2)
	})

	// Test searching with two tags (one result)
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.ServiceMultipleTags("foo", []string{"bar", "v2"}, nil)

		require.NoError(r, err)
		require.NotEqual(r, meta.LastIndex, 0)

		// Should be exactly 1 service, named "foo2"
		require.Len(r, services, 1)
		require.Equal(r, services[0].ServiceID, "foo2")
	})
}

func TestAPI_CatalogService_NodeMetaFilter(t *testing.T) {
	t.Parallel()
	meta := map[string]string{"somekey": "somevalue"}
	c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.NodeMeta = meta
	})
	defer s.Stop()

	catalog := c.Catalog()
	retry.Run(t, func(r *retry.R) {
		services, meta, err := catalog.Service("consul", "", &QueryOptions{NodeMeta: meta})
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}

		if services[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", services[0])
		}
	})
}

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

	// this sets up the catalog entries with things we can filter on
	testNodeServiceCheckRegistrations(t, c, "dc1")

	catalog := c.Catalog()

	services, _, err := catalog.Service("redis", "", &QueryOptions{Filter: "ServiceMeta.version == 1"})
	require.NoError(t, err)
	// finds it on both foo and bar nodes
	require.Len(t, services, 2)

	require.Condition(t, func() bool {
		return (services[0].Node == "foo" && services[1].Node == "bar") ||
			(services[0].Node == "bar" && services[1].Node == "foo")
	})

	services, _, err = catalog.Service("redis", "", &QueryOptions{Filter: "NodeMeta.os != windows"})
	require.NoError(t, err)
	// finds both service instances on foo
	require.Len(t, services, 2)
	require.Equal(t, "foo", services[0].Node)
	require.Equal(t, "foo", services[1].Node)

	services, _, err = catalog.Service("redis", "", &QueryOptions{Filter: "Address == `10.0.0.1`"})
	require.NoError(t, err)
	require.Empty(t, services)

}

func testUpstreams(t *testing.T) []Upstream {
	return []Upstream{
		{
			DestinationName: "db",
			LocalBindPort:   9191,
			Config: map[string]interface{}{
				"connect_timeout_ms": float64(1000),
			},
		},
		{
			DestinationType: UpstreamDestTypePreparedQuery,
			DestinationName: "geo-cache",
			LocalBindPort:   8181,
		},
	}
}

func testExpectUpstreamsWithDefaults(t *testing.T, upstreams []Upstream) []Upstream {
	ups := make([]Upstream, len(upstreams))
	for i := range upstreams {
		ups[i] = upstreams[i]
		// Fill in default fields we expect to have back explicitly in a response
		if ups[i].DestinationType == "" {
			ups[i].DestinationType = UpstreamDestTypeService
		}
	}
	return ups
}

// testUnmanagedProxy returns a fully configured external proxy service suitable
// for checking that all the config fields make it back in a response intact.
func testUnmanagedProxy(t *testing.T) *AgentService {
	return &AgentService{
		Kind: ServiceKindConnectProxy,
		Proxy: &AgentServiceConnectProxyConfig{
			DestinationServiceName: "web",
			DestinationServiceID:   "web1",
			LocalServiceAddress:    "127.0.0.2",
			LocalServicePort:       8080,
			Upstreams:              testUpstreams(t),
			Mode:                   ProxyModeTransparent,
			TransparentProxy: &TransparentProxyConfig{
				OutboundListenerPort: 808,
			},
		},
		ID:      "web-proxy1",
		Service: "web-proxy",
		Port:    8001,
	}
}

// testUnmanagedProxyRegistration returns a *CatalogRegistration for a fully
// configured external proxy.
func testUnmanagedProxyRegistration(t *testing.T) *CatalogRegistration {
	return &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		Service:    testUnmanagedProxy(t),
	}
}

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

	catalog := c.Catalog()

	// Register service and proxy instances to test against.
	proxyReg := testUnmanagedProxyRegistration(t)

	proxy := proxyReg.Service

	service := &AgentService{
		ID:      proxyReg.Service.Proxy.DestinationServiceID,
		Service: proxyReg.Service.Proxy.DestinationServiceName,
		Port:    8000,
	}
	check := &AgentCheck{
		Node:      "foobar",
		CheckID:   "service:" + service.ID,
		Name:      "Redis health check",
		Notes:     "Script based health check",
		Status:    HealthPassing,
		ServiceID: service.ID,
	}

	reg := &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		Service:    service,
		Check:      check,
	}

	retry.Run(t, func(r *retry.R) {
		if _, err := catalog.Register(reg, nil); err != nil {
			r.Fatal(err)
		}
		if _, err := catalog.Register(proxyReg, nil); err != nil {
			r.Fatal(err)
		}

		services, meta, err := catalog.Connect(proxyReg.Service.Proxy.DestinationServiceName, "", nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}

		if services[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", services[0])
		}

		if services[0].ServicePort != proxy.Port {
			r.Fatalf("Returned port should be for proxy: %v", services[0])
		}

		if !reflect.DeepEqual(services[0].ServiceProxy, proxy.Proxy) {
			r.Fatalf("Returned proxy config should match:\nWant: %v\n Got: %v",
				proxy.Proxy, services[0].ServiceProxy)
		}
	})
}

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

	catalog := c.Catalog()

	// Register service and proxy instances to test against.
	service := &AgentService{
		ID:      "redis1",
		Service: "redis",
		Port:    8000,
		Connect: &AgentServiceConnect{Native: true},
	}
	check := &AgentCheck{
		Node:      "foobar",
		CheckID:   "service:redis1",
		Name:      "Redis health check",
		Notes:     "Script based health check",
		Status:    HealthPassing,
		ServiceID: "redis1",
	}

	reg := &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		Service:    service,
		Check:      check,
	}

	retry.Run(t, func(r *retry.R) {
		if _, err := catalog.Register(reg, nil); err != nil {
			r.Fatal(err)
		}

		services, meta, err := catalog.Connect("redis", "", nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(services) == 0 {
			r.Fatalf("Bad: %v", services)
		}

		if services[0].Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", services[0])
		}

		if services[0].ServicePort != service.Port {
			r.Fatalf("Returned port should be for proxy: %v", services[0])
		}
	})
}

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

	// this sets up the catalog entries with things we can filter on
	testNodeServiceCheckRegistrations(t, c, "dc1")

	catalog := c.Catalog()

	services, _, err := catalog.Connect("web", "", &QueryOptions{Filter: "ServicePort == 443"})
	require.NoError(t, err)
	require.Len(t, services, 2)
	require.Condition(t, func() bool {
		return (services[0].Node == "bar" && services[1].Node == "baz") ||
			(services[0].Node == "baz" && services[1].Node == "bar")
	})

	// All the web-connect services are native
	services, _, err = catalog.Connect("web", "", &QueryOptions{Filter: "ServiceConnect.Native != true"})
	require.NoError(t, err)
	require.Empty(t, services)
}

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

	catalog := c.Catalog()

	name, err := c.Agent().NodeName()
	require.NoError(t, err)

	proxyReg := testUnmanagedProxyRegistration(t)
	proxyReg.Node = name
	proxyReg.SkipNodeUpdate = true

	retry.Run(t, func(r *retry.R) {
		// Register a connect proxy to ensure all it's config fields are returned
		_, err := catalog.Register(proxyReg, nil)
		r.Check(err)

		info, meta, err := catalog.Node(name, nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(info.Services) != 2 {
			r.Fatalf("Bad: %v (len %d)", info, len(info.Services))
		}

		if _, ok := info.Node.TaggedAddresses["wan"]; !ok {
			r.Fatalf("Bad: %v", info.Node.TaggedAddresses)
		}

		if info.Node.Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", info)
		}

		if _, ok := info.Services["web-proxy1"]; !ok {
			r.Fatalf("Missing proxy service: %v", info.Services)
		}

		if !reflect.DeepEqual(proxyReg.Service.Proxy, info.Services["web-proxy1"].Proxy) {
			r.Fatalf("Bad proxy config:\nwant %v\n got: %v", proxyReg.Service.Proxy,
				info.Services["web-proxy"].Proxy)
		}
	})
}

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

	catalog := c.Catalog()

	name, err := c.Agent().NodeName()
	require.NoError(t, err)

	proxyReg := testUnmanagedProxyRegistration(t)
	proxyReg.Node = name
	proxyReg.SkipNodeUpdate = true

	retry.Run(t, func(r *retry.R) {
		// Register a connect proxy to ensure all it's config fields are returned
		_, err := catalog.Register(proxyReg, nil)
		r.Check(err)

		info, meta, err := catalog.NodeServiceList(name, nil)
		if err != nil {
			r.Fatal(err)
		}

		if meta.LastIndex == 0 {
			r.Fatalf("Bad: %v", meta)
		}

		if len(info.Services) != 2 {
			r.Fatalf("Bad: %v (len %d)", info, len(info.Services))
		}

		if _, ok := info.Node.TaggedAddresses["wan"]; !ok {
			r.Fatalf("Bad: %v", info.Node.TaggedAddresses)
		}

		if info.Node.Datacenter != "dc1" {
			r.Fatalf("Bad datacenter: %v", info)
		}

		var proxySvc *AgentService
		for _, svc := range info.Services {
			if svc.ID == "web-proxy1" {
				proxySvc = svc
				break
			}
		}

		if proxySvc == nil {
			r.Fatalf("Missing proxy service: %v", info.Services)
			return
		}

		if !reflect.DeepEqual(proxyReg.Service.Proxy, proxySvc.Proxy) {
			r.Fatalf("Bad proxy config:\nwant %v\n got: %v", proxyReg.Service.Proxy,
				proxySvc.Proxy)
		}
	})
}

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

	// this sets up the catalog entries with things we can filter on
	testNodeServiceCheckRegistrations(t, c, "dc1")

	catalog := c.Catalog()

	// should have only 1 matching service
	info, _, err := catalog.Node("bar", &QueryOptions{Filter: "connect in Tags"})
	require.NoError(t, err)
	require.Len(t, info.Services, 1)
	require.Contains(t, info.Services, "webV1")
	require.Equal(t, "web", info.Services["webV1"].Service)

	// should get two services for the node
	info, _, err = catalog.Node("baz", &QueryOptions{Filter: "connect in Tags"})
	require.NoError(t, err)
	require.Len(t, info.Services, 2)
}

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

	catalog := c.Catalog()

	service := &AgentService{
		ID:      "redis1",
		Service: "redis",
		Tags:    []string{"primary", "v1"},
		Port:    8000,
	}

	check := &AgentCheck{
		Node:      "foobar",
		CheckID:   "service:redis1-a",
		Name:      "Redis health check",
		Notes:     "Script based health check",
		Status:    HealthPassing,
		ServiceID: "redis1",
	}

	checks := HealthChecks{
		&HealthCheck{
			Node:      "foobar",
			CheckID:   "service:redis1-b",
			Name:      "Redis health check",
			Notes:     "Script based health check",
			Status:    HealthPassing,
			ServiceID: "redis1",
		},
	}

	reg := &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		NodeMeta:   map[string]string{"somekey": "somevalue"},
		Service:    service,
		// Specifying both Check and Checks is accepted by Consul
		Check:  check,
		Checks: checks,
	}
	// Register a connect proxy for that service too
	proxy := &AgentService{
		ID:      "redis-proxy1",
		Service: "redis-proxy",
		Port:    8001,
		Kind:    ServiceKindConnectProxy,
		Proxy: &AgentServiceConnectProxyConfig{
			DestinationServiceName: service.Service,
		},
	}
	proxyReg := &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		NodeMeta:   map[string]string{"somekey": "somevalue"},
		Service:    proxy,
	}
	retry.Run(t, func(r *retry.R) {
		if _, err := catalog.Register(reg, nil); err != nil {
			r.Fatal(err)
		}
		if _, err := catalog.Register(proxyReg, nil); err != nil {
			r.Fatal(err)
		}

		node, _, err := catalog.Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if _, ok := node.Services["redis1"]; !ok {
			r.Fatal("missing service: redis1")
		}

		if _, ok := node.Services["redis-proxy1"]; !ok {
			r.Fatal("missing service: redis-proxy1")
		}

		health, _, err := c.Health().Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if health[0].CheckID != "service:redis1-a" {
			r.Fatal("missing checkid service:redis1-a")
		}

		if health[1].CheckID != "service:redis1-b" {
			r.Fatal("missing checkid service:redis1-b")
		}

		if v, ok := node.Node.Meta["somekey"]; !ok || v != "somevalue" {
			r.Fatal("missing node meta pair somekey:somevalue")
		}
	})

	// Test catalog deregistration of the previously registered service
	dereg := &CatalogDeregistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		ServiceID:  "redis1",
	}

	// ... and proxy
	deregProxy := &CatalogDeregistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		ServiceID:  "redis-proxy1",
	}

	if _, err := catalog.Deregister(dereg, nil); err != nil {
		t.Fatalf("err: %v", err)
	}

	if _, err := catalog.Deregister(deregProxy, nil); err != nil {
		t.Fatalf("err: %v", err)
	}

	retry.Run(t, func(r *retry.R) {
		node, _, err := catalog.Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if _, ok := node.Services["redis1"]; ok {
			r.Fatal("ServiceID:redis1 is not deregistered")
		}

		if _, ok := node.Services["redis-proxy1"]; ok {
			r.Fatal("ServiceID:redis-proxy1 is not deregistered")
		}
	})

	// Test deregistration of the previously registered check
	dereg = &CatalogDeregistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		CheckID:    "service:redis1-a",
	}

	if _, err := catalog.Deregister(dereg, nil); err != nil {
		t.Fatalf("err: %v", err)
	}

	dereg = &CatalogDeregistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		CheckID:    "service:redis1-b",
	}

	if _, err := catalog.Deregister(dereg, nil); err != nil {
		t.Fatalf("err: %v", err)
	}

	retry.Run(t, func(r *retry.R) {
		health, _, err := c.Health().Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if len(health) != 0 {
			r.Fatal("CheckID:service:redis1-a or CheckID:service:redis1-a is not deregistered")
		}
	})

	// Test node deregistration of the previously registered node
	dereg = &CatalogDeregistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
	}

	if _, err := catalog.Deregister(dereg, nil); err != nil {
		t.Fatalf("err: %v", err)
	}

	retry.Run(t, func(r *retry.R) {
		node, _, err := catalog.Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if node != nil {
			r.Fatalf("node is not deregistered: %v", node)
		}
	})
}

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

	catalog := c.Catalog()

	service := &AgentService{
		ID:      "redis1",
		Service: "redis",
		Tags:    []string{"primary", "v1"},
		Port:    8000,
	}

	reg := &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "foobar",
		Address:    "192.168.10.10",
		Service:    service,
	}

	retry.Run(t, func(r *retry.R) {
		if _, err := catalog.Register(reg, nil); err != nil {
			r.Fatal(err)
		}

		node, _, err := catalog.Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if _, ok := node.Services["redis1"]; !ok {
			r.Fatal("missing service: redis1")
		}
		if node.Services["redis1"].EnableTagOverride != false {
			r.Fatal("tag override set")
		}

		services, _, err := catalog.Service("redis", "", nil)
		if err != nil {
			r.Fatal(err)
		}

		if len(services) < 1 || services[0].ServiceName != "redis" {
			r.Fatal("missing service: redis")
		}
		if services[0].ServiceEnableTagOverride != false {
			r.Fatal("tag override set")
		}
	})

	service.EnableTagOverride = true

	retry.Run(t, func(r *retry.R) {
		if _, err := catalog.Register(reg, nil); err != nil {
			r.Fatal(err)
		}

		node, _, err := catalog.Node("foobar", nil)
		if err != nil {
			r.Fatal(err)
		}

		if _, ok := node.Services["redis1"]; !ok {
			r.Fatal("missing service: redis1")
		}
		if node.Services["redis1"].EnableTagOverride != true {
			r.Fatal("tag override not set")
		}

		services, _, err := catalog.Service("redis", "", nil)
		if err != nil {
			r.Fatal(err)
		}

		if len(services) < 1 || services[0].ServiceName != "redis" {
			r.Fatal("missing service: redis")
		}
		if services[0].ServiceEnableTagOverride != true {
			r.Fatal("tag override not set")
		}
	})
}

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

	catalog := c.Catalog()

	// Register a service to be covered by a wildcard in the config entry
	svc := &AgentService{
		ID:      "redis",
		Service: "redis",
		Port:    6379,
	}
	reg := &CatalogRegistration{
		Datacenter: "dc1",
		Node:       "bar",
		Address:    "192.168.10.11",
		Service:    svc,
	}
	retry.Run(t, func(r *retry.R) {
		if _, err := catalog.Register(reg, nil); err != nil {
			r.Fatal(err)
		}
	})

	entries := c.ConfigEntries()

	// Associate the gateway and api/redis services
	gwEntry := TerminatingGatewayConfigEntry{
		Kind: TerminatingGateway,
		Name: "terminating",
		Services: []LinkedService{
			{
				Name:     "api",
				CAFile:   "api/ca.crt",
				CertFile: "api/client.crt",
				KeyFile:  "api/client.key",
				SNI:      "my-domain",
			},
			{
				Name:     "*",
				CAFile:   "ca.crt",
				CertFile: "client.crt",
				KeyFile:  "client.key",
				SNI:      "my-alt-domain",
			},
		},
	}
	retry.Run(t, func(r *retry.R) {
		if success, _, err := entries.Set(&gwEntry, nil); err != nil || !success {
			r.Fatal(err)
		}
	})

	expect := []*GatewayService{
		{
			Service:     CompoundServiceName{Name: "api", Namespace: defaultNamespace, Partition: defaultPartition},
			Gateway:     CompoundServiceName{Name: "terminating", Namespace: defaultNamespace, Partition: defaultPartition},
			GatewayKind: ServiceKindTerminatingGateway,
			CAFile:      "api/ca.crt",
			CertFile:    "api/client.crt",
			KeyFile:     "api/client.key",
			SNI:         "my-domain",
		},
		{
			Service:      CompoundServiceName{Name: "redis", Namespace: defaultNamespace, Partition: defaultPartition},
			Gateway:      CompoundServiceName{Name: "terminating", Namespace: defaultNamespace, Partition: defaultPartition},
			GatewayKind:  ServiceKindTerminatingGateway,
			CAFile:       "ca.crt",
			CertFile:     "client.crt",
			KeyFile:      "client.key",
			SNI:          "my-alt-domain",
			FromWildcard: true,
		},
	}
	retry.Run(t, func(r *retry.R) {
		resp, _, err := catalog.GatewayServices("terminating", nil)
		assert.NoError(r, err)
		assert.Equal(r, expect, resp)
	})
}

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

	s.WaitForSerfCheck(t)

	entries := c.ConfigEntries()

	// Associate the gateway and api/redis services
	gwEntry := IngressGatewayConfigEntry{
		Kind: "ingress-gateway",
		Name: "ingress",
		Listeners: []IngressListener{
			{
				Port: 8888,
				Services: []IngressService{
					{
						Name: "api",
					},
				},
			},
			{
				Port: 9999,
				Services: []IngressService{
					{
						Name: "redis",
					},
				},
			},
		},
	}
	retry.Run(t, func(r *retry.R) {
		if success, _, err := entries.Set(&gwEntry, nil); err != nil || !success {
			r.Fatal(err)
		}
	})

	catalog := c.Catalog()

	expect := []*GatewayService{
		{
			Service:     CompoundServiceName{Name: "api", Namespace: defaultNamespace, Partition: defaultPartition},
			Gateway:     CompoundServiceName{Name: "ingress", Namespace: defaultNamespace, Partition: defaultPartition},
			GatewayKind: ServiceKindIngressGateway,
			Protocol:    "tcp",
			Port:        8888,
		},
		{
			Service:     CompoundServiceName{Name: "redis", Namespace: defaultNamespace, Partition: defaultPartition},
			Gateway:     CompoundServiceName{Name: "ingress", Namespace: defaultNamespace, Partition: defaultPartition},
			GatewayKind: ServiceKindIngressGateway,
			Protocol:    "tcp",
			Port:        9999,
		},
	}
	retry.Run(t, func(r *retry.R) {
		resp, _, err := catalog.GatewayServices("ingress", nil)
		assert.NoError(r, err)
		assert.Equal(r, expect, resp)
	})
}
