package consul

import (
	"sort"
	"testing"
	"text/template"

	"github.com/BurntSushi/ty/fun"
	"github.com/containous/traefik/types"
	"github.com/hashicorp/consul/api"
	"github.com/stretchr/testify/assert"
)

func TestConsulCatalogGetFrontendRule(t *testing.T) {
	testCases := []struct {
		desc     string
		service  serviceUpdate
		expected string
	}{
		{
			desc: "Should return default host foo.localhost",
			service: serviceUpdate{
				ServiceName: "foo",
				Attributes:  []string{},
			},
			expected: "Host:foo.localhost",
		},
		{
			desc: "Should return host *.example.com",
			service: serviceUpdate{
				ServiceName: "foo",
				Attributes: []string{
					"traefik.frontend.rule=Host:*.example.com",
				},
			},
			expected: "Host:*.example.com",
		},
		{
			desc: "Should return host foo.example.com",
			service: serviceUpdate{
				ServiceName: "foo",
				Attributes: []string{
					"traefik.frontend.rule=Host:{{.ServiceName}}.example.com",
				},
			},
			expected: "Host:foo.example.com",
		},
		{
			desc: "Should return path prefix /bar",
			service: serviceUpdate{
				ServiceName: "foo",
				Attributes: []string{
					"traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}",
					"contextPath=/bar",
				},
			},
			expected: "PathPrefix:/bar",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			provider := &CatalogProvider{
				Domain:               "localhost",
				Prefix:               "traefik",
				FrontEndRule:         "Host:{{.ServiceName}}.{{.Domain}}",
				frontEndRuleTemplate: template.New("consul catalog frontend rule"),
			}
			provider.setupFrontEndTemplate()

			actual := provider.getFrontendRule(test.service)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetTag(t *testing.T) {
	provider := &CatalogProvider{
		Domain: "localhost",
		Prefix: "traefik",
	}

	testCases := []struct {
		desc         string
		tags         []string
		key          string
		defaultValue string
		expected     string
	}{
		{
			desc: "Should return value of foo.bar key",
			tags: []string{
				"foo.bar=random",
				"traefik.backend.weight=42",
				"management",
			},
			key:          "foo.bar",
			defaultValue: "0",
			expected:     "random",
		},
	}

	assert.Equal(t, true, provider.hasTag("management", []string{"management"}))
	assert.Equal(t, true, provider.hasTag("management", []string{"management=yes"}))

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := provider.getTag(test.key, test.tags, test.defaultValue)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetAttribute(t *testing.T) {
	provider := &CatalogProvider{
		Domain: "localhost",
		Prefix: "traefik",
	}

	testCases := []struct {
		desc         string
		tags         []string
		key          string
		defaultValue string
		expected     string
	}{
		{
			desc: "Should return tag value 42",
			tags: []string{
				"foo.bar=ramdom",
				"traefik.backend.weight=42",
			},
			key:          "backend.weight",
			defaultValue: "0",
			expected:     "42",
		},
		{
			desc: "Should return tag default value 0",
			tags: []string{
				"foo.bar=ramdom",
				"traefik.backend.wei=42",
			},
			key:          "backend.weight",
			defaultValue: "0",
			expected:     "0",
		},
	}

	assert.Equal(t, provider.Prefix+".foo", provider.getPrefixedName("foo"))

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := provider.getAttribute(test.key, test.tags, test.defaultValue)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) {
	provider := &CatalogProvider{
		Domain: "localhost",
		Prefix: "",
	}

	testCases := []struct {
		desc         string
		tags         []string
		key          string
		defaultValue string
		expected     string
	}{
		{
			desc: "Should return tag value 42",
			tags: []string{
				"foo.bar=ramdom",
				"backend.weight=42",
			},
			key:          "backend.weight",
			defaultValue: "0",
			expected:     "42",
		},
		{
			desc: "Should return default value 0",
			tags: []string{
				"foo.bar=ramdom",
				"backend.wei=42",
			},
			key:          "backend.weight",
			defaultValue: "0",
			expected:     "0",
		},
		{
			desc: "Should return for.bar key value random",
			tags: []string{
				"foo.bar=ramdom",
				"backend.wei=42",
			},
			key:          "foo.bar",
			defaultValue: "random",
			expected:     "ramdom",
		},
	}

	assert.Equal(t, "foo", provider.getPrefixedName("foo"))

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := provider.getAttribute(test.key, test.tags, test.defaultValue)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetBackendAddress(t *testing.T) {
	provider := &CatalogProvider{
		Domain: "localhost",
		Prefix: "traefik",
	}

	testCases := []struct {
		desc     string
		node     *api.ServiceEntry
		expected string
	}{
		{
			desc: "Should return the address of the service",
			node: &api.ServiceEntry{
				Node: &api.Node{
					Address: "10.1.0.1",
				},
				Service: &api.AgentService{
					Address: "10.2.0.1",
				},
			},
			expected: "10.2.0.1",
		},
		{
			desc: "Should return the address of the node",
			node: &api.ServiceEntry{
				Node: &api.Node{
					Address: "10.1.0.1",
				},
				Service: &api.AgentService{
					Address: "",
				},
			},
			expected: "10.1.0.1",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := provider.getBackendAddress(test.node)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetBackendName(t *testing.T) {
	provider := &CatalogProvider{
		Domain: "localhost",
		Prefix: "traefik",
	}

	testCases := []struct {
		desc     string
		node     *api.ServiceEntry
		expected string
	}{
		{
			desc: "Should create backend name without tags",
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{},
				},
			},
			expected: "api--10-0-0-1--80--0",
		},
		{
			desc: "Should create backend name with multiple tags",
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{"traefik.weight=42", "traefik.enable=true"},
				},
			},
			expected: "api--10-0-0-1--80--traefik-weight-42--traefik-enable-true--1",
		},
		{
			desc: "Should create backend name with one tag",
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{"a funny looking tag"},
				},
			},
			expected: "api--10-0-0-1--80--a-funny-looking-tag--2",
		},
	}

	for i, test := range testCases {
		test := test
		i := i
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := provider.getBackendName(test.node, i)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogBuildConfig(t *testing.T) {
	provider := &CatalogProvider{
		Domain:               "localhost",
		Prefix:               "traefik",
		ExposedByDefault:     false,
		FrontEndRule:         "Host:{{.ServiceName}}.{{.Domain}}",
		frontEndRuleTemplate: template.New("consul catalog frontend rule"),
	}

	testCases := []struct {
		desc              string
		nodes             []catalogUpdate
		expectedFrontends map[string]*types.Frontend
		expectedBackends  map[string]*types.Backend
	}{
		{
			desc:              "Should build config of nothing",
			nodes:             []catalogUpdate{},
			expectedFrontends: map[string]*types.Frontend{},
			expectedBackends:  map[string]*types.Backend{},
		},
		{
			desc: "Should build config with no frontend and backend",
			nodes: []catalogUpdate{
				{
					Service: &serviceUpdate{
						ServiceName: "test",
					},
				},
			},
			expectedFrontends: map[string]*types.Frontend{},
			expectedBackends:  map[string]*types.Backend{},
		},
		{
			desc: "Should build config who contains one frontend and one backend",
			nodes: []catalogUpdate{
				{
					Service: &serviceUpdate{
						ServiceName: "test",
						Attributes: []string{
							"traefik.backend.loadbalancer=drr",
							"traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5",
							"random.foo=bar",
							"traefik.backend.maxconn.amount=1000",
							"traefik.backend.maxconn.extractorfunc=client.ip",
							"traefik.frontend.auth.basic=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
						},
					},
					Nodes: []*api.ServiceEntry{
						{
							Service: &api.AgentService{
								Service: "test",
								Address: "127.0.0.1",
								Port:    80,
								Tags: []string{
									"traefik.backend.weight=42",
									"random.foo=bar",
									"traefik.backend.passHostHeader=true",
									"traefik.protocol=https",
								},
							},
							Node: &api.Node{
								Node:    "localhost",
								Address: "127.0.0.1",
							},
						},
					},
				},
			},
			expectedFrontends: map[string]*types.Frontend{
				"frontend-test": {
					Backend:        "backend-test",
					PassHostHeader: true,
					Routes: map[string]types.Route{
						"route-host-test": {
							Rule: "Host:test.localhost",
						},
					},
					BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-test": {
					Servers: map[string]types.Server{
						"test--127-0-0-1--80--traefik-backend-weight-42--random-foo-bar--traefik-backend-passHostHeader-true--traefik-protocol-https--0": {
							URL:    "https://127.0.0.1:80",
							Weight: 42,
						},
					},
					CircuitBreaker: &types.CircuitBreaker{
						Expression: "NetworkErrorRatio() > 0.5",
					},
					LoadBalancer: &types.LoadBalancer{
						Method: "drr",
					},
					MaxConn: &types.MaxConn{
						Amount:        1000,
						ExtractorFunc: "client.ip",
					},
				},
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actualConfig := provider.buildConfig(test.nodes)
			assert.Equal(t, test.expectedBackends, actualConfig.Backends)
			assert.Equal(t, test.expectedFrontends, actualConfig.Frontends)
		})
	}
}

func TestConsulCatalogNodeSorter(t *testing.T) {
	testCases := []struct {
		desc     string
		nodes    []*api.ServiceEntry
		expected []*api.ServiceEntry
	}{
		{
			desc:     "Should sort nothing",
			nodes:    []*api.ServiceEntry{},
			expected: []*api.ServiceEntry{},
		},
		{
			desc: "Should sort by node address",
			nodes: []*api.ServiceEntry{
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "127.0.0.1",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.1",
					},
				},
			},
			expected: []*api.ServiceEntry{
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "127.0.0.1",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.1",
					},
				},
			},
		},
		{
			desc: "Should sort by service name",
			nodes: []*api.ServiceEntry{
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "127.0.0.2",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
				{
					Service: &api.AgentService{
						Service: "bar",
						Address: "127.0.0.2",
						Port:    81,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "127.0.0.1",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.1",
					},
				},
				{
					Service: &api.AgentService{
						Service: "bar",
						Address: "127.0.0.2",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
			},
			expected: []*api.ServiceEntry{
				{
					Service: &api.AgentService{
						Service: "bar",
						Address: "127.0.0.2",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
				{
					Service: &api.AgentService{
						Service: "bar",
						Address: "127.0.0.2",
						Port:    81,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "127.0.0.1",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.1",
					},
				},
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "127.0.0.2",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
			},
		},
		{
			desc: "Should sort by node address",
			nodes: []*api.ServiceEntry{
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.1",
					},
				},
			},
			expected: []*api.ServiceEntry{
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.1",
					},
				},
				{
					Service: &api.AgentService{
						Service: "foo",
						Address: "",
						Port:    80,
					},
					Node: &api.Node{
						Node:    "localhost",
						Address: "127.0.0.2",
					},
				},
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			sort.Sort(nodeSorter(test.nodes))
			actual := test.nodes
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetChangedKeys(t *testing.T) {
	type Input struct {
		currState map[string]Service
		prevState map[string]Service
	}

	type Output struct {
		addedKeys   []string
		removedKeys []string
	}

	testCases := []struct {
		desc   string
		input  Input
		output Output
	}{
		{
			desc: "Should add 0 services and removed 0",
			input: Input{
				currState: map[string]Service{
					"foo-service":    {Name: "v1"},
					"bar-service":    {Name: "v1"},
					"baz-service":    {Name: "v1"},
					"qux-service":    {Name: "v1"},
					"quux-service":   {Name: "v1"},
					"quuz-service":   {Name: "v1"},
					"corge-service":  {Name: "v1"},
					"grault-service": {Name: "v1"},
					"garply-service": {Name: "v1"},
					"waldo-service":  {Name: "v1"},
					"fred-service":   {Name: "v1"},
					"plugh-service":  {Name: "v1"},
					"xyzzy-service":  {Name: "v1"},
					"thud-service":   {Name: "v1"},
				},
				prevState: map[string]Service{
					"foo-service":    {Name: "v1"},
					"bar-service":    {Name: "v1"},
					"baz-service":    {Name: "v1"},
					"qux-service":    {Name: "v1"},
					"quux-service":   {Name: "v1"},
					"quuz-service":   {Name: "v1"},
					"corge-service":  {Name: "v1"},
					"grault-service": {Name: "v1"},
					"garply-service": {Name: "v1"},
					"waldo-service":  {Name: "v1"},
					"fred-service":   {Name: "v1"},
					"plugh-service":  {Name: "v1"},
					"xyzzy-service":  {Name: "v1"},
					"thud-service":   {Name: "v1"},
				},
			},
			output: Output{
				addedKeys:   []string{},
				removedKeys: []string{},
			},
		},
		{
			desc: "Should add 3 services and removed 0",
			input: Input{
				currState: map[string]Service{
					"foo-service":    {Name: "v1"},
					"bar-service":    {Name: "v1"},
					"baz-service":    {Name: "v1"},
					"qux-service":    {Name: "v1"},
					"quux-service":   {Name: "v1"},
					"quuz-service":   {Name: "v1"},
					"corge-service":  {Name: "v1"},
					"grault-service": {Name: "v1"},
					"garply-service": {Name: "v1"},
					"waldo-service":  {Name: "v1"},
					"fred-service":   {Name: "v1"},
					"plugh-service":  {Name: "v1"},
					"xyzzy-service":  {Name: "v1"},
					"thud-service":   {Name: "v1"},
				},
				prevState: map[string]Service{
					"foo-service":    {Name: "v1"},
					"bar-service":    {Name: "v1"},
					"baz-service":    {Name: "v1"},
					"corge-service":  {Name: "v1"},
					"grault-service": {Name: "v1"},
					"garply-service": {Name: "v1"},
					"waldo-service":  {Name: "v1"},
					"fred-service":   {Name: "v1"},
					"plugh-service":  {Name: "v1"},
					"xyzzy-service":  {Name: "v1"},
					"thud-service":   {Name: "v1"},
				},
			},
			output: Output{
				addedKeys:   []string{"qux-service", "quux-service", "quuz-service"},
				removedKeys: []string{},
			},
		},
		{
			desc: "Should add 2 services and removed 2",
			input: Input{
				currState: map[string]Service{
					"foo-service":    {Name: "v1"},
					"qux-service":    {Name: "v1"},
					"quux-service":   {Name: "v1"},
					"quuz-service":   {Name: "v1"},
					"corge-service":  {Name: "v1"},
					"grault-service": {Name: "v1"},
					"garply-service": {Name: "v1"},
					"waldo-service":  {Name: "v1"},
					"fred-service":   {Name: "v1"},
					"plugh-service":  {Name: "v1"},
					"xyzzy-service":  {Name: "v1"},
					"thud-service":   {Name: "v1"},
				},
				prevState: map[string]Service{
					"foo-service":   {Name: "v1"},
					"bar-service":   {Name: "v1"},
					"baz-service":   {Name: "v1"},
					"qux-service":   {Name: "v1"},
					"quux-service":  {Name: "v1"},
					"quuz-service":  {Name: "v1"},
					"corge-service": {Name: "v1"},
					"waldo-service": {Name: "v1"},
					"fred-service":  {Name: "v1"},
					"plugh-service": {Name: "v1"},
					"xyzzy-service": {Name: "v1"},
					"thud-service":  {Name: "v1"},
				},
			},
			output: Output{
				addedKeys:   []string{"grault-service", "garply-service"},
				removedKeys: []string{"bar-service", "baz-service"},
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			addedKeys, removedKeys := getChangedServiceKeys(test.input.currState, test.input.prevState)
			assert.Equal(t, fun.Set(test.output.addedKeys), fun.Set(addedKeys), "Added keys comparison results: got %q, want %q", addedKeys, test.output.addedKeys)
			assert.Equal(t, fun.Set(test.output.removedKeys), fun.Set(removedKeys), "Removed keys comparison results: got %q, want %q", removedKeys, test.output.removedKeys)
		})
	}
}

func TestConsulCatalogFilterEnabled(t *testing.T) {
	testCases := []struct {
		desc             string
		exposedByDefault bool
		node             *api.ServiceEntry
		expected         bool
	}{
		{
			desc:             "exposed",
			exposedByDefault: true,
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{""},
				},
			},
			expected: true,
		},
		{
			desc:             "exposed and tolerated by valid label value",
			exposedByDefault: true,
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{"", "traefik.enable=true"},
				},
			},
			expected: true,
		},
		{
			desc:             "exposed and tolerated by invalid label value",
			exposedByDefault: true,
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{"", "traefik.enable=bad"},
				},
			},
			expected: true,
		},
		{
			desc:             "exposed but overridden by label",
			exposedByDefault: true,
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{"", "traefik.enable=false"},
				},
			},
			expected: false,
		},
		{
			desc:             "non-exposed",
			exposedByDefault: false,
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{""},
				},
			},
			expected: false,
		},
		{
			desc:             "non-exposed but overridden by label",
			exposedByDefault: false,
			node: &api.ServiceEntry{
				Service: &api.AgentService{
					Service: "api",
					Address: "10.0.0.1",
					Port:    80,
					Tags:    []string{"", "traefik.enable=true"},
				},
			},
			expected: true,
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()
			provider := &CatalogProvider{
				Domain:           "localhost",
				Prefix:           "traefik",
				ExposedByDefault: test.exposedByDefault,
			}
			actual := provider.nodeFilter("test", test.node)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetBasicAuth(t *testing.T) {
	testCases := []struct {
		desc     string
		tags     []string
		expected []string
	}{
		{
			desc:     "label missing",
			tags:     []string{},
			expected: []string{},
		},
		{
			desc: "label existing",
			tags: []string{
				"traefik.frontend.auth.basic=user:password",
			},
			expected: []string{"user:password"},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()
			provider := &CatalogProvider{
				Prefix: "traefik",
			}
			actual := provider.getBasicAuth(test.tags)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogHasStickinessLabel(t *testing.T) {
	testCases := []struct {
		desc     string
		tags     []string
		expected bool
	}{
		{
			desc:     "label missing",
			tags:     []string{},
			expected: false,
		},
		{
			desc: "stickiness=true",
			tags: []string{
				types.LabelBackendLoadbalancerStickiness + "=true",
			},
			expected: true,
		},
		{
			desc: "stickiness=false",
			tags: []string{
				types.LabelBackendLoadbalancerStickiness + "=false",
			},
			expected: false,
		},
	}

	provider := &CatalogProvider{
		Prefix: "traefik",
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := provider.hasStickinessLabel(test.tags)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogGetChangedStringKeys(t *testing.T) {
	testCases := []struct {
		desc            string
		current         []string
		previous        []string
		expectedAdded   []string
		expectedRemoved []string
	}{
		{
			desc:            "1 element added, 0 removed",
			current:         []string{"chou"},
			previous:        []string{},
			expectedAdded:   []string{"chou"},
			expectedRemoved: []string{},
		}, {
			desc:            "0 element added, 0 removed",
			current:         []string{"chou"},
			previous:        []string{"chou"},
			expectedAdded:   []string{},
			expectedRemoved: []string{},
		},
		{
			desc:            "0 element added, 1 removed",
			current:         []string{},
			previous:        []string{"chou"},
			expectedAdded:   []string{},
			expectedRemoved: []string{"chou"},
		},
		{
			desc:            "1 element added, 1 removed",
			current:         []string{"carotte"},
			previous:        []string{"chou"},
			expectedAdded:   []string{"carotte"},
			expectedRemoved: []string{"chou"},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actualAdded, actualRemoved := getChangedStringKeys(test.current, test.previous)
			assert.Equal(t, test.expectedAdded, actualAdded)
			assert.Equal(t, test.expectedRemoved, actualRemoved)
		})
	}
}

func TestConsulCatalogHasNodeOrTagschanged(t *testing.T) {
	testCases := []struct {
		desc     string
		current  map[string]Service
		previous map[string]Service
		expected bool
	}{
		{
			desc: "Change detected due to change of nodes",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node2"},
					Tags:  []string{},
				},
			},
			expected: true,
		},
		{
			desc:    "No change missing current service",
			current: make(map[string]Service),
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			expected: false,
		},
		{
			desc: "No change on nodes",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			expected: false,
		},
		{
			desc: "No change on nodes and tags",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
				},
			},
			expected: false,
		},
		{
			desc: "Change detected on tags",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo"},
				},
			},
			expected: true,
		},
		{
			desc: "Change detected on ports",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
					Ports: []int{80},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo"},
					Ports: []int{81},
				},
			},
			expected: true,
		},
		{
			desc: "Change detected on ports",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
					Ports: []int{80},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo"},
					Ports: []int{81, 82},
				},
			},
			expected: true,
		},
		{
			desc: "No Change detected",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo"},
					Ports: []int{80},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo"},
					Ports: []int{80},
				},
			},
			expected: false,
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := hasServiceChanged(test.current, test.previous)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestConsulCatalogHasChanged(t *testing.T) {
	testCases := []struct {
		desc     string
		current  map[string]Service
		previous map[string]Service
		expected bool
	}{
		{
			desc: "Change detected due to change new service",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			previous: make(map[string]Service),
			expected: true,
		},
		{
			desc:    "Change detected due to change service removed",
			current: make(map[string]Service),
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			expected: true,
		},
		{
			desc: "Change detected due to change of nodes",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node2"},
					Tags:  []string{},
				},
			},
			expected: true,
		},
		{
			desc: "No change on nodes",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{},
				},
			},
			expected: false,
		},
		{
			desc: "No change on nodes and tags",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
				},
			},
			expected: false,
		},
		{
			desc: "Change detected on tags",
			current: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo=bar"},
				},
			},
			previous: map[string]Service{
				"foo-service": {
					Name:  "foo",
					Nodes: []string{"node1"},
					Tags:  []string{"foo"},
				},
			},
			expected: true,
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := hasChanged(test.current, test.previous)
			assert.Equal(t, test.expected, actual)
		})
	}
}
