package kv

import (
	"sort"
	"strconv"
	"testing"
	"time"

	"github.com/abronan/valkeyrie/store"
	"github.com/containous/flaeg"
	"github.com/containous/traefik/provider/label"
	"github.com/containous/traefik/tls"
	"github.com/containous/traefik/types"
	"github.com/stretchr/testify/assert"
)

func aKVPair(key string, value string) *store.KVPair {
	return &store.KVPair{Key: key, Value: []byte(value)}
}

func TestProviderBuildConfiguration(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		expected *types.Configuration
	}{
		{
			desc: "name with dot",
			kvPairs: filler("traefik",
				frontend("frontend.with.dot",
					withPair("backend", "backend.with.dot.too"),
					withPair("routes/route.with.dot/rule", "Host:test.localhost")),
				backend("backend.with.dot.too",
					withPair("servers/server.with.dot/url", "http://172.17.0.2:80"),
					withPair("servers/server.with.dot/weight", strconv.Itoa(label.DefaultWeight)),
					withPair("servers/server.with.dot.without.url/weight", strconv.Itoa(label.DefaultWeight))),
			),
			expected: &types.Configuration{
				Backends: map[string]*types.Backend{
					"backend.with.dot.too": {
						LoadBalancer: &types.LoadBalancer{Method: label.DefaultBackendLoadBalancerMethod},
						Servers: map[string]types.Server{
							"server.with.dot": {
								URL:    "http://172.17.0.2:80",
								Weight: label.DefaultWeight,
							},
						},
					},
				},
				Frontends: map[string]*types.Frontend{
					"frontend.with.dot": {
						Backend:        "backend.with.dot.too",
						PassHostHeader: true,
						EntryPoints:    []string{},
						BasicAuth:      []string{},
						Routes: map[string]types.Route{
							"route.with.dot": {
								Rule: "Host:test.localhost",
							},
						},
					},
				},
			},
		},
		{
			desc: "all parameters",
			kvPairs: filler("traefik",
				backend("backend1",
					withPair(pathBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression),
					withPair(pathBackendLoadBalancerMethod, "drr"),
					withPair(pathBackendLoadBalancerSticky, "true"),
					withPair(pathBackendLoadBalancerStickiness, "true"),
					withPair(pathBackendLoadBalancerStickinessCookieName, "tomate"),
					withPair(pathBackendHealthCheckPath, "/health"),
					withPair(pathBackendHealthCheckPort, "80"),
					withPair(pathBackendHealthCheckInterval, "30s"),
					withPair(pathBackendMaxConnAmount, "5"),
					withPair(pathBackendMaxConnExtractorFunc, "client.ip"),
					withPair(pathBackendBufferingMaxResponseBodyBytes, "10485760"),
					withPair(pathBackendBufferingMemResponseBodyBytes, "2097152"),
					withPair(pathBackendBufferingMaxRequestBodyBytes, "10485760"),
					withPair(pathBackendBufferingMemRequestBodyBytes, "2097152"),
					withPair(pathBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"),
					withPair("servers/server1/url", "http://172.17.0.2:80"),
					withPair("servers/server1/weight", strconv.Itoa(label.DefaultWeight)),
					withPair("servers/server2/weight", strconv.Itoa(label.DefaultWeight))),
				frontend("frontend1",
					withPair(pathFrontendBackend, "backend1"),
					withPair(pathFrontendPriority, "6"),
					withPair(pathFrontendPassHostHeader, "false"),
					withPair(pathFrontendPassTLSCert, "true"),
					withPair(pathFrontendEntryPoints, "http,https"),
					withPair(pathFrontendWhiteListSourceRange, "1.1.1.1/24, 1234:abcd::42/32"),
					withPair(pathFrontendWhiteListUseXForwardedFor, "true"),
					withPair(pathFrontendBasicAuth, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/, test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"),
					withPair(pathFrontendRedirectEntryPoint, "https"),
					withPair(pathFrontendRedirectRegex, "nope"),
					withPair(pathFrontendRedirectReplacement, "nope"),
					withPair(pathFrontendRedirectPermanent, "true"),
					withErrorPage("foo", "error", "/test1", "500-501, 503-599"),
					withErrorPage("bar", "error", "/test2", "400-405"),
					withRateLimit("client.ip",
						withLimit("foo", "6", "12", "18"),
						withLimit("bar", "3", "6", "9")),

					withPair(pathFrontendCustomRequestHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendCustomRequestHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendCustomRequestHeaders+"X-Custom-Header", "test"),
					withPair(pathFrontendCustomResponseHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendCustomResponseHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendCustomResponseHeaders+"X-Custom-Header", "test"),
					withPair(pathFrontendSSLProxyHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendSSLProxyHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendSSLProxyHeaders+"X-Custom-Header", "test"),
					withPair(pathFrontendAllowedHosts, "example.com, ssl.example.com"),
					withPair(pathFrontendHostsProxyHeaders, "foo, bar, goo, hor"),
					withPair(pathFrontendSTSSeconds, "666"),
					withPair(pathFrontendSSLHost, "foo"),
					withPair(pathFrontendCustomFrameOptionsValue, "foo"),
					withPair(pathFrontendContentSecurityPolicy, "foo"),
					withPair(pathFrontendPublicKey, "foo"),
					withPair(pathFrontendReferrerPolicy, "foo"),
					withPair(pathFrontendCustomBrowserXSSValue, "foo"),
					withPair(pathFrontendSSLRedirect, "true"),
					withPair(pathFrontendSSLTemporaryRedirect, "true"),
					withPair(pathFrontendSTSIncludeSubdomains, "true"),
					withPair(pathFrontendSTSPreload, "true"),
					withPair(pathFrontendForceSTSHeader, "true"),
					withPair(pathFrontendFrameDeny, "true"),
					withPair(pathFrontendContentTypeNosniff, "true"),
					withPair(pathFrontendBrowserXSSFilter, "true"),
					withPair(pathFrontendIsDevelopment, "true"),

					withPair("routes/route1/rule", "Host:test.localhost"),
					withPair("routes/route2/rule", "Path:/foo")),
				entry("tls/foo",
					withPair("entrypoints", "http,https"),
					withPair("certificate/certfile", "certfile1"),
					withPair("certificate/keyfile", "keyfile1")),
				entry("tls/bar",
					withPair("entrypoints", "http,https"),
					withPair("certificate/certfile", "certfile2"),
					withPair("certificate/keyfile", "keyfile2")),
			),
			expected: &types.Configuration{
				Backends: map[string]*types.Backend{
					"backend1": {
						Servers: map[string]types.Server{
							"server1": {
								URL:    "http://172.17.0.2:80",
								Weight: label.DefaultWeight,
							},
						},
						CircuitBreaker: &types.CircuitBreaker{
							Expression: "NetworkErrorRatio() > 1",
						},
						LoadBalancer: &types.LoadBalancer{
							Method: "drr",
							Sticky: true,
							Stickiness: &types.Stickiness{
								CookieName: "tomate",
							},
						},
						MaxConn: &types.MaxConn{
							Amount:        5,
							ExtractorFunc: "client.ip",
						},
						HealthCheck: &types.HealthCheck{
							Path:     "/health",
							Port:     80,
							Interval: "30s",
						},
						Buffering: &types.Buffering{
							MaxResponseBodyBytes: 10485760,
							MemResponseBodyBytes: 2097152,
							MaxRequestBodyBytes:  10485760,
							MemRequestBodyBytes:  2097152,
							RetryExpression:      "IsNetworkError() && Attempts() <= 2",
						},
					},
				},
				Frontends: map[string]*types.Frontend{
					"frontend1": {
						Priority:    6,
						EntryPoints: []string{"http", "https"},
						Backend:     "backend1",
						PassTLSCert: true,
						WhiteList: &types.WhiteList{
							SourceRange:      []string{"1.1.1.1/24", "1234:abcd::42/32"},
							UseXForwardedFor: true,
						},
						BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
						Redirect: &types.Redirect{
							EntryPoint: "https",
							Permanent:  true,
						},
						Errors: map[string]*types.ErrorPage{
							"foo": {
								Backend: "error",
								Query:   "/test1",
								Status:  []string{"500-501", "503-599"},
							},
							"bar": {
								Backend: "error",
								Query:   "/test2",
								Status:  []string{"400-405"},
							},
						},
						RateLimit: &types.RateLimit{
							ExtractorFunc: "client.ip",
							RateSet: map[string]*types.Rate{
								"foo": {
									Average: 6,
									Burst:   12,
									Period:  flaeg.Duration(18 * time.Second),
								},
								"bar": {
									Average: 3,
									Burst:   6,
									Period:  flaeg.Duration(9 * time.Second),
								},
							},
						},
						Routes: map[string]types.Route{
							"route1": {
								Rule: "Host:test.localhost",
							},
							"route2": {
								Rule: "Path:/foo",
							},
						},
						Headers: &types.Headers{
							CustomRequestHeaders: map[string]string{
								"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
								"Content-Type":                 "application/json; charset=utf-8",
								"X-Custom-Header":              "test",
							},
							CustomResponseHeaders: map[string]string{
								"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
								"Content-Type":                 "application/json; charset=utf-8",
								"X-Custom-Header":              "test",
							},
							SSLProxyHeaders: map[string]string{
								"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
								"Content-Type":                 "application/json; charset=utf-8",
								"X-Custom-Header":              "test",
							},
							AllowedHosts:            []string{"example.com", "ssl.example.com"},
							HostsProxyHeaders:       []string{"foo", "bar", "goo", "hor"},
							STSSeconds:              666,
							SSLHost:                 "foo",
							CustomFrameOptionsValue: "foo",
							ContentSecurityPolicy:   "foo",
							PublicKey:               "foo",
							ReferrerPolicy:          "foo",
							CustomBrowserXSSValue:   "foo",
							SSLRedirect:             true,
							SSLTemporaryRedirect:    true,
							STSIncludeSubdomains:    true,
							STSPreload:              true,
							ForceSTSHeader:          true,
							FrameDeny:               true,
							ContentTypeNosniff:      true,
							BrowserXSSFilter:        true,
							IsDevelopment:           true,
						},
					},
				},
				TLS: []*tls.Configuration{
					{
						EntryPoints: []string{"http", "https"},
						Certificate: &tls.Certificate{
							CertFile: "certfile2",
							KeyFile:  "keyfile2",
						},
					},
					{
						EntryPoints: []string{"http", "https"},
						Certificate: &tls.Certificate{
							CertFile: "certfile1",
							KeyFile:  "keyfile1",
						},
					},
				},
			},
		},
	}

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

			p := &Provider{
				Prefix: "traefik",
				kvClient: &Mock{
					KVPairs: test.kvPairs,
				},
			}

			actual := p.buildConfiguration()
			assert.NotNil(t, actual)

			assert.EqualValues(t, test.expected.Backends, actual.Backends)
			assert.EqualValues(t, test.expected.Frontends, actual.Frontends)
			assert.EqualValues(t, test.expected, actual)
		})
	}
}

func TestProviderList(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected []string
	}{
		{
			desc:     "empty key parts and empty store",
			keyParts: []string{},
			expected: []string{},
		},
		{
			desc:     "when non existing key and empty store",
			keyParts: []string{"traefik"},
			expected: []string{},
		},
		{
			desc: "when non existing key",
			kvPairs: []*store.KVPair{
				aKVPair("foo", "bar"),
			},
			keyParts: []string{"bar"},
			expected: []string{},
		},
		{
			desc: "when one key",
			kvPairs: []*store.KVPair{
				aKVPair("foo", "bar"),
			},
			keyParts: []string{"foo"},
			expected: []string{"foo"},
		},
		{
			desc: "when multiple sub keys and nested sub key",
			kvPairs: []*store.KVPair{
				aKVPair("foo/baz/1", "bar"),
				aKVPair("foo/baz/2", "bar"),
				aKVPair("foo/baz/biz/1", "bar"),
			},
			keyParts: []string{"foo", "/baz/"},
			expected: []string{"foo/baz/1", "foo/baz/2"},
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: []*store.KVPair{
				aKVPair("foo/baz/1", "bar1"),
				aKVPair("foo/baz/2", "bar2"),
				aKVPair("foo/baz/biz/1", "bar3"),
			},
			keyParts: []string{"foo/baz/1"},
			expected: nil,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			actual := p.list(test.keyParts...)

			sort.Strings(test.expected)
			assert.Equal(t, test.expected, actual, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGet(t *testing.T) {
	testCases := []struct {
		desc         string
		kvPairs      []*store.KVPair
		storeType    store.Backend
		keyParts     []string
		defaultValue string
		kvError      error
		expected     string
	}{
		{
			desc:         "when empty key parts, empty store",
			defaultValue: "circle",
			keyParts:     []string{},
			expected:     "circle",
		},
		{
			desc:         "when non existing key",
			defaultValue: "circle",
			kvPairs: []*store.KVPair{
				aKVPair("foo", "bar"),
			},
			keyParts: []string{"bar"},
			expected: "circle",
		},
		{
			desc: "when one part key",
			kvPairs: []*store.KVPair{
				aKVPair("foo", "bar"),
			},
			keyParts: []string{"foo"},
			expected: "bar",
		},
		{
			desc: "when several parts key",
			kvPairs: []*store.KVPair{
				aKVPair("foo/baz/1", "bar1"),
				aKVPair("foo/baz/2", "bar2"),
				aKVPair("foo/baz/biz/1", "bar3"),
			},
			keyParts: []string{"foo", "/baz/", "2"},
			expected: "bar2",
		},
		{
			desc:         "when several parts key, starts with /",
			defaultValue: "circle",
			kvPairs: []*store.KVPair{
				aKVPair("foo/baz/1", "bar1"),
				aKVPair("foo/baz/2", "bar2"),
				aKVPair("foo/baz/biz/1", "bar3"),
			},
			keyParts: []string{"/foo", "/baz/", "2"},
			expected: "circle",
		},
		{
			desc:      "when several parts key starts with /, ETCD v2",
			storeType: store.ETCD,
			kvPairs: []*store.KVPair{
				aKVPair("foo/baz/1", "bar1"),
				aKVPair("foo/baz/2", "bar2"),
				aKVPair("foo/baz/biz/1", "bar3"),
			},
			keyParts: []string{"/foo", "/baz/", "2"},
			expected: "bar2",
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: []*store.KVPair{
				aKVPair("foo/baz/1", "bar1"),
				aKVPair("foo/baz/2", "bar2"),
				aKVPair("foo/baz/biz/1", "bar3"),
			},
			keyParts: []string{"foo/baz/1"},
			expected: "",
		},
	}

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

			p := &Provider{
				kvClient:  newKvClientMock(test.kvPairs, test.kvError),
				storeType: test.storeType,
			}

			actual := p.get(test.defaultValue, test.keyParts...)

			assert.Equal(t, test.expected, actual, "key %v", test.keyParts)
		})
	}
}

func TestProviderLast(t *testing.T) {
	p := &Provider{}

	testCases := []struct {
		key      string
		expected string
	}{
		{
			key:      "",
			expected: "",
		},
		{
			key:      "foo",
			expected: "foo",
		},
		{
			key:      "foo/bar",
			expected: "bar",
		},
		{
			key:      "foo/bar/baz",
			expected: "baz",
		},
		// FIXME is this wanted ?
		{
			key:      "foo/bar/",
			expected: "",
		},
	}

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

			actual := p.last(test.key)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestProviderSplitGet(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected []string
	}{
		{
			desc: "when has value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "courgette, carotte, tomate, aubergine"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: []string{"courgette", "carotte", "tomate", "aubergine"},
		},
		{
			desc: "when empty value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: nil,
		},
		{
			desc:     "when not existing key",
			kvPairs:  nil,
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: nil,
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: nil,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			values := p.splitGet(test.keyParts...)

			assert.Equal(t, test.expected, values, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGetList(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected []string
	}{
		{
			desc: "comma separated",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("entrypoints", "courgette, carotte, tomate, aubergine"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: []string{"courgette", "carotte", "tomate", "aubergine"},
		},
		{
			desc: "multiple entries",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("entrypoints/0", "courgette"),
					withPair("entrypoints/1", "carotte"),
					withPair("entrypoints/2", "tomate"),
					withPair("entrypoints/3", "aubergine"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: []string{"courgette", "carotte", "tomate", "aubergine"},
		},
		{
			desc: "when empty value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
		{
			desc:     "when not existing key",
			kvPairs:  nil,
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			values := p.getList(test.keyParts...)

			assert.Equal(t, test.expected, values, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGetSlice(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected []string
	}{
		{
			desc: "multiple entries",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("entrypoints/0", "courgette"),
					withPair("entrypoints/1", "carotte"),
					withPair("entrypoints/2", "tomate"),
					withPair("entrypoints/3", "aubergine"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: []string{"courgette", "carotte", "tomate", "aubergine"},
		},
		{
			desc: "comma separated",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("entrypoints", "courgette, carotte, tomate, aubergine"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
		{
			desc: "when empty value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
		{
			desc:     "when not existing key",
			kvPairs:  nil,
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/entrypoints"},
			expected: nil,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			values := p.getSlice(test.keyParts...)

			assert.Equal(t, test.expected, values, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGetBool(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected bool
	}{
		{
			desc: "when value is 'true",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "true"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: true,
		},
		{
			desc: "when value is 'false",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "false"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: false,
		},
		{
			desc: "when empty value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: false,
		},
		{
			desc:     "when not existing key",
			kvPairs:  nil,
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: false,
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "true"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: false,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			actual := p.getBool(false, test.keyParts...)

			assert.Equal(t, test.expected, actual, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGetInt(t *testing.T) {
	defaultValue := 666

	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected int
	}{
		{
			desc: "when has value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "6"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: 6,
		},
		{
			desc: "when empty value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: defaultValue,
		},
		{
			desc:     "when not existing key",
			kvPairs:  nil,
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: defaultValue,
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "true"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: defaultValue,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			actual := p.getInt(defaultValue, test.keyParts...)

			assert.Equal(t, test.expected, actual, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGetInt64(t *testing.T) {
	var defaultValue int64 = 666

	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		kvError  error
		keyParts []string
		expected int64
	}{
		{
			desc: "when has value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "6"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: 6,
		},
		{
			desc: "when empty value",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", ""),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: defaultValue,
		},
		{
			desc:     "when not existing key",
			kvPairs:  nil,
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: defaultValue,
		},
		{
			desc:    "when KV error",
			kvError: store.ErrNotReachable,
			kvPairs: filler("traefik",
				frontend("foo",
					withPair("bar", "true"),
				),
			),
			keyParts: []string{"traefik/frontends/foo/bar"},
			expected: defaultValue,
		},
	}

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

			p := &Provider{
				kvClient: newKvClientMock(test.kvPairs, test.kvError),
			}

			actual := p.getInt64(defaultValue, test.keyParts...)

			assert.Equal(t, test.expected, actual, "key: %v", test.keyParts)
		})
	}
}

func TestProviderGetMap(t *testing.T) {
	testCases := []struct {
		desc     string
		keyParts []string
		kvPairs  []*store.KVPair
		expected map[string]string
	}{
		{
			desc:     "when several keys",
			keyParts: []string{"traefik/frontends/foo", pathFrontendCustomRequestHeaders},
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendCustomRequestHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendCustomRequestHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendCustomRequestHeaders+"X-Custom-Header", "test"),
				),
			),
			expected: map[string]string{
				"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
				"Content-Type":                 "application/json; charset=utf-8",
				"X-Custom-Header":              "test",
			},
		},
		{
			desc:     "when no keys",
			keyParts: []string{"traefik/frontends/foo", pathFrontendCustomRequestHeaders},
			kvPairs:  filler("traefik", frontend("foo")),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getMap(test.keyParts...)

			assert.EqualValues(t, test.expected, result)
		})
	}
}

func TestProviderHasStickinessLabel(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		rootPath string
		expected bool
	}{
		{
			desc:     "without option",
			expected: false,
		},
		{
			desc:     "with cookie name without stickiness=true",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerStickinessCookieName, "aubergine"),
				),
			),
			expected: false,
		},
		{
			desc:     "stickiness=true",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerStickiness, "true"),
				),
			),
			expected: true,
		},
	}

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

			p := &Provider{
				kvClient: &Mock{
					KVPairs: test.kvPairs,
				},
			}

			actual := p.hasStickinessLabel(test.rootPath)

			if actual != test.expected {
				t.Fatalf("expected %v, got %v", test.expected, actual)
			}
		})
	}
}

func TestWhiteList(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.WhiteList
	}{
		{
			desc:     "should return nil when no white list labels",
			rootPath: "traefik/frontends/foo",
			expected: nil,
		},
		{
			desc:     "should return a struct when only range",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendWhiteListSourceRange, "10.10.10.10"))),
			expected: &types.WhiteList{
				SourceRange: []string{
					"10.10.10.10",
				},
				UseXForwardedFor: false,
			},
		},
		{
			desc:     "should return a struct when range and UseXForwardedFor",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendWhiteListSourceRange, "10.10.10.10"),
					withPair(pathFrontendWhiteListUseXForwardedFor, "true"))),
			expected: &types.WhiteList{
				SourceRange: []string{
					"10.10.10.10",
				},
				UseXForwardedFor: true,
			},
		},
		{
			desc:     "should return nil when only UseXForwardedFor",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendWhiteListUseXForwardedFor, "true"))),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			actual := p.getWhiteList(test.rootPath)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestProviderGetRedirect(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.Redirect
	}{
		{
			desc:     "should use entry point when entry point key is valued in the store",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRedirectEntryPoint, "https"))),
			expected: &types.Redirect{
				EntryPoint: "https",
			},
		},
		{
			desc:     "should use entry point when entry point key is valued in the store (permanent)",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRedirectEntryPoint, "https"),
					withPair(pathFrontendRedirectPermanent, "true"))),
			expected: &types.Redirect{
				EntryPoint: "https",
				Permanent:  true,
			},
		},
		{
			desc:     "should use regex when regex keys are valued in the store",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRedirectRegex, "(.*)"),
					withPair(pathFrontendRedirectReplacement, "$1"))),
			expected: &types.Redirect{
				Regex:       "(.*)",
				Replacement: "$1",
			},
		},
		{
			desc:     "should use regex when regex keys are valued in the store (permanent)",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRedirectRegex, "(.*)"),
					withPair(pathFrontendRedirectReplacement, "$1"),
					withPair(pathFrontendRedirectPermanent, "true"))),
			expected: &types.Redirect{
				Regex:       "(.*)",
				Replacement: "$1",
				Permanent:   true,
			},
		},
		{
			desc:     "should only use entry point when entry point and regex base are valued in the store",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRedirectEntryPoint, "https"),
					withPair(pathFrontendRedirectRegex, "nope"),
					withPair(pathFrontendRedirectReplacement, "nope"))),
			expected: &types.Redirect{
				EntryPoint: "https",
			},
		},
		{
			desc:     "should return when redirect keys are not valued in the store",
			rootPath: "traefik/frontends/foo",
			kvPairs:  filler("traefik", frontend("foo")),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			actual := p.getRedirect(test.rootPath)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestProviderGetErrorPages(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected map[string]*types.ErrorPage
	}{
		{
			desc:     "2 errors pages",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withErrorPage("foo", "error", "/test1", "500-501, 503-599"),
					withErrorPage("bar", "error", "/test2", "400-405"))),
			expected: map[string]*types.ErrorPage{
				"foo": {
					Backend: "error",
					Query:   "/test1",
					Status:  []string{"500-501", "503-599"},
				},
				"bar": {
					Backend: "error",
					Query:   "/test2",
					Status:  []string{"400-405"},
				},
			},
		},
		{
			desc:     "return nil when no errors pages",
			rootPath: "traefik/frontends/foo",
			kvPairs:  filler("traefik", frontend("foo")),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			actual := p.getErrorPages(test.rootPath)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestProviderGetRateLimit(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.RateLimit
	}{
		{
			desc:     "with several limits",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withRateLimit("client.ip",
						withLimit("foo", "6", "12", "18"),
						withLimit("bar", "3", "6", "9")))),
			expected: &types.RateLimit{
				ExtractorFunc: "client.ip",
				RateSet: map[string]*types.Rate{
					"foo": {
						Average: 6,
						Burst:   12,
						Period:  flaeg.Duration(18 * time.Second),
					},
					"bar": {
						Average: 3,
						Burst:   6,
						Period:  flaeg.Duration(9 * time.Second),
					},
				},
			},
		},
		{
			desc:     "return nil when no extractor func",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withRateLimit("",
						withLimit("foo", "6", "12", "18"),
						withLimit("bar", "3", "6", "9")))),
			expected: nil,
		},
		{
			desc:     "return nil when no rate limit keys",
			rootPath: "traefik/frontends/foo",
			kvPairs:  filler("traefik", frontend("foo")),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			actual := p.getRateLimit(test.rootPath)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestProviderGetHeaders(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.Headers
	}{
		{
			desc:     "Custom Request Headers",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendCustomRequestHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendCustomRequestHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendCustomRequestHeaders+"X-Custom-Header", "test"))),
			expected: &types.Headers{
				CustomRequestHeaders: map[string]string{
					"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
					"Content-Type":                 "application/json; charset=utf-8",
					"X-Custom-Header":              "test",
				},
			},
		},
		{
			desc:     "Custom esponse Headers",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendCustomResponseHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendCustomResponseHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendCustomResponseHeaders+"X-Custom-Header", "test"))),
			expected: &types.Headers{
				CustomResponseHeaders: map[string]string{
					"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
					"Content-Type":                 "application/json; charset=utf-8",
					"X-Custom-Header":              "test",
				},
			},
		},
		{
			desc:     "SSL Proxy Headers",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSSLProxyHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
					withPair(pathFrontendSSLProxyHeaders+"Content-Type", "application/json; charset=utf-8"),
					withPair(pathFrontendSSLProxyHeaders+"X-Custom-Header", "test"))),
			expected: &types.Headers{
				SSLProxyHeaders: map[string]string{
					"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
					"Content-Type":                 "application/json; charset=utf-8",
					"X-Custom-Header":              "test",
				},
			},
		},
		{
			desc:     "Allowed Hosts",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendAllowedHosts, "foo, bar, goo, hor"))),
			expected: &types.Headers{
				AllowedHosts: []string{"foo", "bar", "goo", "hor"},
			},
		},
		{
			desc:     "Hosts Proxy Headers",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendHostsProxyHeaders, "foo, bar, goo, hor"))),
			expected: &types.Headers{
				HostsProxyHeaders: []string{"foo", "bar", "goo", "hor"},
			},
		},
		{
			desc:     "SSL Redirect",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSSLRedirect, "true"))),
			expected: &types.Headers{
				SSLRedirect: true,
			},
		},
		{
			desc:     "SSL Temporary Redirect",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSSLTemporaryRedirect, "true"))),
			expected: &types.Headers{
				SSLTemporaryRedirect: true,
			},
		},
		{
			desc:     "SSL Host",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSSLHost, "foo"))),
			expected: &types.Headers{
				SSLHost: "foo",
			},
		},
		{
			desc:     "STS Seconds",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSTSSeconds, "666"))),
			expected: &types.Headers{
				STSSeconds: 666,
			},
		},
		{
			desc:     "STS Include Subdomains",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSTSIncludeSubdomains, "true"))),
			expected: &types.Headers{
				STSIncludeSubdomains: true,
			},
		},
		{
			desc:     "STS Preload",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendSTSPreload, "true"))),
			expected: &types.Headers{
				STSPreload: true,
			},
		},
		{
			desc:     "Force STS Header",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendForceSTSHeader, "true"))),
			expected: &types.Headers{
				ForceSTSHeader: true,
			},
		},
		{
			desc:     "Frame Deny",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendFrameDeny, "true"))),
			expected: &types.Headers{
				FrameDeny: true,
			},
		},
		{
			desc:     "Custom Frame Options Value",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendCustomFrameOptionsValue, "foo"))),
			expected: &types.Headers{
				CustomFrameOptionsValue: "foo",
			},
		},
		{
			desc:     "Content Type Nosniff",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendContentTypeNosniff, "true"))),
			expected: &types.Headers{
				ContentTypeNosniff: true,
			},
		},
		{
			desc:     "Browser XSS Filter",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendBrowserXSSFilter, "true"))),
			expected: &types.Headers{
				BrowserXSSFilter: true,
			},
		},
		{
			desc:     "Custom Browser XSS Value",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendCustomBrowserXSSValue, "foo"))),
			expected: &types.Headers{
				CustomBrowserXSSValue: "foo",
			},
		},
		{
			desc:     "Content Security Policy",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendContentSecurityPolicy, "foo"))),
			expected: &types.Headers{
				ContentSecurityPolicy: "foo",
			},
		},
		{
			desc:     "Public Key",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendPublicKey, "foo"))),
			expected: &types.Headers{
				PublicKey: "foo",
			},
		},
		{
			desc:     "Referrer Policy",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendReferrerPolicy, "foo"))),
			expected: &types.Headers{
				ReferrerPolicy: "foo",
			},
		},
		{
			desc:     "Is Development",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendIsDevelopment, "true"))),
			expected: &types.Headers{
				IsDevelopment: true,
			},
		},
		{
			desc:     "should return nil when not significant configuration",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendIsDevelopment, "false"))),
			expected: nil,
		},
		{
			desc:     "should return nil when no headers configuration",
			rootPath: "traefik/frontends/foo",
			kvPairs:  filler("traefik", frontend("foo")),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			headers := p.getHeaders(test.rootPath)

			assert.Equal(t, test.expected, headers)
		})
	}
}

func TestProviderGetLoadBalancer(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.LoadBalancer
	}{
		{
			desc:     "when all keys",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerMethod, "drr"),
					withPair(pathBackendLoadBalancerSticky, "true"),
					withPair(pathBackendLoadBalancerStickiness, "true"),
					withPair(pathBackendLoadBalancerStickinessCookieName, "aubergine"))),
			expected: &types.LoadBalancer{
				Method: "drr",
				Sticky: true,
				Stickiness: &types.Stickiness{
					CookieName: "aubergine",
				},
			},
		},
		{
			desc:     "when no specific configuration",
			rootPath: "traefik/backends/foo",
			kvPairs:  filler("traefik", backend("foo")),
			expected: &types.LoadBalancer{
				Method: "wrr",
			},
		},
		{
			desc:     "when method is set",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerMethod, "drr"))),
			expected: &types.LoadBalancer{
				Method: "drr",
			},
		},
		{
			desc:     "when sticky is set",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerSticky, "true"))),
			expected: &types.LoadBalancer{
				Method: "wrr",
				Sticky: true,
			},
		},
		{
			desc:     "when stickiness is set",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerStickiness, "true"))),
			expected: &types.LoadBalancer{
				Method:     "wrr",
				Stickiness: &types.Stickiness{},
			},
		},
		{
			desc:     "when stickiness cookie name is set",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerStickiness, "true"),
					withPair(pathBackendLoadBalancerStickinessCookieName, "aubergine"))),
			expected: &types.LoadBalancer{
				Method: "wrr",
				Stickiness: &types.Stickiness{
					CookieName: "aubergine",
				},
			},
		},
		{
			desc:     "when stickiness cookie name is set but not stickiness",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendLoadBalancerStickinessCookieName, "aubergine"))),
			expected: &types.LoadBalancer{
				Method: "wrr",
			},
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getLoadBalancer(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetCircuitBreaker(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.CircuitBreaker
	}{
		{
			desc:     "when cb expression defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression))),
			expected: &types.CircuitBreaker{
				Expression: label.DefaultCircuitBreakerExpression,
			},
		},
		{
			desc:     "when no cb expression",
			rootPath: "traefik/backends/foo",
			kvPairs:  filler("traefik", backend("foo")),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getCircuitBreaker(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetMaxConn(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.MaxConn
	}{
		{
			desc:     "when max conn keys are defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendMaxConnAmount, "5"),
					withPair(pathBackendMaxConnExtractorFunc, "client.ip"))),
			expected: &types.MaxConn{
				Amount:        5,
				ExtractorFunc: "client.ip",
			},
		},
		{
			desc:     "should return nil when only extractor func is defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendMaxConnExtractorFunc, "client.ip"))),
			expected: nil,
		},
		{
			desc:     "when only amount is defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendMaxConnAmount, "5"))),
			expected: &types.MaxConn{
				Amount:        5,
				ExtractorFunc: "request.host",
			},
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getMaxConn(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetHealthCheck(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.HealthCheck
	}{
		{
			desc:     "when all configuration keys defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendHealthCheckPath, "/health"),
					withPair(pathBackendHealthCheckPort, "80"),
					withPair(pathBackendHealthCheckInterval, "10s"))),
			expected: &types.HealthCheck{
				Interval: "10s",
				Path:     "/health",
				Port:     80,
			},
		},
		{
			desc:     "when only path defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendHealthCheckPath, "/health"))),
			expected: &types.HealthCheck{
				Interval: "30s",
				Path:     "/health",
				Port:     0,
			},
		},
		{
			desc:     "should return nil when no path",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendHealthCheckPort, "80"),
					withPair(pathBackendHealthCheckInterval, "30s"))),
			expected: nil,
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getHealthCheck(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetBufferingReal(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected *types.Buffering
	}{
		{
			desc:     "when all configuration keys defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendBufferingMaxResponseBodyBytes, "10485760"),
					withPair(pathBackendBufferingMemResponseBodyBytes, "2097152"),
					withPair(pathBackendBufferingMaxRequestBodyBytes, "10485760"),
					withPair(pathBackendBufferingMemRequestBodyBytes, "2097152"),
					withPair(pathBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"))),
			expected: &types.Buffering{
				MaxResponseBodyBytes: 10485760,
				MemResponseBodyBytes: 2097152,
				MaxRequestBodyBytes:  10485760,
				MemRequestBodyBytes:  2097152,
				RetryExpression:      "IsNetworkError() && Attempts() <= 2",
			},
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getBuffering(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetTLSes(t *testing.T) {
	testCases := []struct {
		desc     string
		kvPairs  []*store.KVPair
		expected []*tls.Configuration
	}{
		{
			desc: "when several TLS configuration defined",
			kvPairs: filler("traefik",
				entry("tls/foo",
					withPair("entrypoints", "http,https"),
					withPair("certificate/certfile", "certfile1"),
					withPair("certificate/keyfile", "keyfile1")),
				entry("tls/bar",
					withPair("entrypoints", "http,https"),
					withPair("certificate/certfile", "certfile2"),
					withPair("certificate/keyfile", "keyfile2"))),
			expected: []*tls.Configuration{
				{
					EntryPoints: []string{"http", "https"},
					Certificate: &tls.Certificate{
						CertFile: "certfile2",
						KeyFile:  "keyfile2",
					},
				},
				{
					EntryPoints: []string{"http", "https"},
					Certificate: &tls.Certificate{
						CertFile: "certfile1",
						KeyFile:  "keyfile1",
					},
				},
			},
		},
		{
			desc:     "should return nil when no TLS configuration",
			kvPairs:  filler("traefik", entry("tls/foo")),
			expected: nil,
		},
		{
			desc: "should return nil when no entry points",
			kvPairs: filler("traefik",
				entry("tls/foo",
					withPair("certificate/certfile", "certfile2"),
					withPair("certificate/keyfile", "keyfile2"))),
			expected: nil,
		},
		{
			desc: "should return nil when no cert file and no key file",
			kvPairs: filler("traefik",
				entry("tls/foo",
					withPair("entrypoints", "http,https"))),
			expected: nil,
		},
	}
	prefix := "traefik"

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

			p := newProviderMock(test.kvPairs)

			result := p.getTLSSection(prefix)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetRoutes(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected map[string]types.Route
	}{
		{
			desc:     "should return nil when no data",
			expected: nil,
		},
		{
			desc:     "should return nil when route key exists but without rule key",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRoutes+"bar", "test1"),
					withPair(pathFrontendRoutes+"bir", "test2"))),
			expected: nil,
		},
		{
			desc:     "should return a map when configuration keys are defined",
			rootPath: "traefik/frontends/foo",
			kvPairs: filler("traefik",
				frontend("foo",
					withPair(pathFrontendRoutes+"bar"+pathFrontendRule, "test1"),
					withPair(pathFrontendRoutes+"bir"+pathFrontendRule, "test2"))),
			expected: map[string]types.Route{
				"bar": {
					Rule: "test1",
				},
				"bir": {
					Rule: "test2",
				},
			},
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getRoutes(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}

func TestProviderGetServers(t *testing.T) {
	testCases := []struct {
		desc     string
		rootPath string
		kvPairs  []*store.KVPair
		expected map[string]types.Server
	}{
		{
			desc:     "should return nil when no data",
			expected: nil,
		},
		{
			desc:     "should return nil when server has no URL",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendServers+"server1/weight", "7"),
					withPair(pathBackendServers+"server2/weight", "6"))),
			expected: nil,
		},
		{
			desc:     "should use default weight when invalid weight value",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendServers+"server1/url", "http://172.17.0.2:80"),
					withPair(pathBackendServers+"server1/weight", "kls"))),
			expected: map[string]types.Server{
				"server1": {
					URL:    "http://172.17.0.2:80",
					Weight: label.DefaultWeight,
				},
			},
		},
		{
			desc:     "should return a map when configuration keys are defined",
			rootPath: "traefik/backends/foo",
			kvPairs: filler("traefik",
				backend("foo",
					withPair(pathBackendServers+"server1/url", "http://172.17.0.2:80"),
					withPair(pathBackendServers+"server2/url", "http://172.17.0.3:80"),
					withPair(pathBackendServers+"server2/weight", "6"))),
			expected: map[string]types.Server{
				"server1": {
					URL:    "http://172.17.0.2:80",
					Weight: label.DefaultWeight,
				},
				"server2": {
					URL:    "http://172.17.0.3:80",
					Weight: 6,
				},
			},
		},
	}

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

			p := newProviderMock(test.kvPairs)

			result := p.getServers(test.rootPath)

			assert.Equal(t, test.expected, result)
		})
	}
}
